Merge "[SP32] Try to get 1.1 OffloadControl hardware if available"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index ef96d88..7c638a2 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -11,6 +11,12 @@
           "exclude-annotation": "com.android.testutils.SkipPresubmit"
         }
       ]
+    },
+    {
+      "name": "TetheringTests"
+    },
+    {
+      "name": "TetheringIntegrationTests"
     }
   ],
   "mainline-presubmit": [
@@ -23,11 +29,36 @@
       ]
     }
   ],
-  // Tests on physical devices with SIM cards: postsubmit only for capacity constraints
   "mainline-postsubmit": [
+    // Tests on physical devices with SIM cards: postsubmit only for capacity constraints
     {
       "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
       "keywords": ["sim"]
+    },
+    {
+      "name": "TetheringCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+    }
+  ],
+  "imports": [
+    {
+      "path": "packages/modules/NetworkStack"
+    }
+  ],
+  "imports": [
+    {
+      "path": "frameworks/base/core/java/android/net"
+    },
+    {
+      "path": "packages/modules/NetworkStack"
+    },
+    {
+      "path": "packages/modules/CaptivePortalLogin"
+    },
+    {
+      "path": "packages/modules/Connectivity"
+    },
+    {
+      "path": "packages/modules/Connectivity/Tethering"
     }
   ]
 }
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 89f1505..6031646 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -30,6 +30,7 @@
         ":services-tethering-shared-srcs",
     ],
     static_libs: [
+        "NetworkStackApiStableShims",
         "androidx.annotation_annotation",
         "modules-utils-build",
         "netlink-client",
@@ -146,14 +147,15 @@
     manifest: "AndroidManifest.xml",
     use_embedded_native_libs: true,
     // The permission configuration *must* be included to ensure security of the device
-    required: ["NetworkPermissionConfig"],
+    required: [
+        "NetworkPermissionConfig",
+        "privapp_whitelist_com.android.networkstack.tethering",
+    ],
     apex_available: ["com.android.tethering"],
     min_sdk_version: "30",
 }
 
 sdk {
     name: "tethering-module-sdk",
-    java_sdk_libs: [
-        "framework-tethering",
-    ],
+    bootclasspath_fragments: ["com.android.tethering-bootclasspath-fragment"],
 }
diff --git a/Tethering/TEST_MAPPING b/Tethering/TEST_MAPPING
deleted file mode 100644
index 5617b0c..0000000
--- a/Tethering/TEST_MAPPING
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "TetheringTests"
-    }
-  ],
-  "postsubmit": [
-    {
-      "name": "TetheringIntegrationTests"
-    }
-  ]
-}
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 164bda4..bd7ebda 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -24,8 +24,10 @@
     // cannot build as updatable unless service-connectivity builds against stable API).
     updatable: false,
     // min_sdk_version: "30",
+    bootclasspath_fragments: [
+        "com.android.tethering-bootclasspath-fragment",
+    ],
     java_libs: [
-        "framework-tethering",
         "service-connectivity",
     ],
     jni_libs: [
@@ -39,8 +41,12 @@
         "ServiceConnectivityResources",
         "Tethering",
     ],
+    prebuilts: ["current_sdkinfo"],
     manifest: "manifest.json",
     key: "com.android.tethering.key",
+    // Indicates that pre-installed version of this apex can be compressed.
+    // Whether it actually will be compressed is controlled on per-device basis.
+    compressible: true,
 
     androidManifest: "AndroidManifest.xml",
 }
@@ -56,6 +62,13 @@
     certificate: "com.android.tethering",
 }
 
+// Encapsulate the contributions made by the com.android.tethering to the bootclasspath.
+bootclasspath_fragment {
+    name: "com.android.tethering-bootclasspath-fragment",
+    contents: ["framework-tethering"],
+    apex_available: ["com.android.tethering"],
+}
+
 override_apex {
     name: "com.android.tethering.inprocess",
     base: "com.android.tethering",
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 e310fb6..33f1c29 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
@@ -179,6 +179,18 @@
     }
 
     @Override
+    public boolean addDevMap(int ifIndex) {
+        /* no op */
+        return false;
+    }
+
+    @Override
+    public boolean removeDevMap(int ifIndex) {
+        /* no op */
+        return false;
+    }
+
+    @Override
     public String toString() {
         return "Netd used";
     }
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 d7ce139..74ddcbc 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
@@ -36,6 +36,8 @@
 import com.android.networkstack.tethering.Tether4Key;
 import com.android.networkstack.tethering.Tether4Value;
 import com.android.networkstack.tethering.Tether6Value;
+import com.android.networkstack.tethering.TetherDevKey;
+import com.android.networkstack.tethering.TetherDevValue;
 import com.android.networkstack.tethering.TetherDownstream6Key;
 import com.android.networkstack.tethering.TetherLimitKey;
 import com.android.networkstack.tethering.TetherLimitValue;
@@ -85,6 +87,10 @@
     @Nullable
     private final BpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
 
+    // BPF map of interface index mapping for XDP.
+    @Nullable
+    private final BpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
+
     // Tracking IPv4 rule count while any rule is using the given upstream interfaces. Used for
     // reducing the BPF map iteration query. The count is increased or decreased when the rule is
     // added or removed successfully on mBpfDownstream4Map. Counting the rules on downstream4 map
@@ -108,6 +114,7 @@
         mBpfUpstream6Map = deps.getBpfUpstream6Map();
         mBpfStatsMap = deps.getBpfStatsMap();
         mBpfLimitMap = deps.getBpfLimitMap();
+        mBpfDevMap = deps.getBpfDevMap();
 
         // Clear the stubs of the maps for handling the system service crash if any.
         // Doesn't throw the exception and clear the stubs as many as possible.
@@ -141,12 +148,18 @@
         } catch (ErrnoException e) {
             mLog.e("Could not clear mBpfLimitMap: " + e);
         }
+        try {
+            if (mBpfDevMap != null) mBpfDevMap.clear();
+        } catch (ErrnoException e) {
+            mLog.e("Could not clear mBpfDevMap: " + e);
+        }
     }
 
     @Override
     public boolean isInitialized() {
         return mBpfDownstream4Map != null && mBpfUpstream4Map != null && mBpfDownstream6Map != null
-                && mBpfUpstream6Map != null && mBpfStatsMap != null && mBpfLimitMap != null;
+                && mBpfUpstream6Map != null && mBpfStatsMap != null && mBpfLimitMap != null
+                && mBpfDevMap != null;
     }
 
     @Override
@@ -432,6 +445,32 @@
         return mRule4CountOnUpstream.get(ifIndex) != null;
     }
 
+    @Override
+    public boolean addDevMap(int ifIndex) {
+        if (!isInitialized()) return false;
+
+        try {
+            mBpfDevMap.updateEntry(new TetherDevKey(ifIndex), new TetherDevValue(ifIndex));
+        } catch (ErrnoException e) {
+            mLog.e("Could not add interface " + ifIndex + ": " + e);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean removeDevMap(int ifIndex) {
+        if (!isInitialized()) return false;
+
+        try {
+            mBpfDevMap.deleteEntry(new TetherDevKey(ifIndex));
+        } catch (ErrnoException e) {
+            mLog.e("Could not delete interface " + ifIndex + ": " + e);
+            return false;
+        }
+        return true;
+    }
+
     private String mapStatus(BpfMap m, String name) {
         return name + "{" + (m != null ? "OK" : "ERROR") + "}";
     }
@@ -444,7 +483,8 @@
                 mapStatus(mBpfDownstream4Map, "mBpfDownstream4Map"),
                 mapStatus(mBpfUpstream4Map, "mBpfUpstream4Map"),
                 mapStatus(mBpfStatsMap, "mBpfStatsMap"),
-                mapStatus(mBpfLimitMap, "mBpfLimitMap")
+                mapStatus(mBpfLimitMap, "mBpfLimitMap"),
+                mapStatus(mBpfDevMap, "mBpfDevMap")
         });
     }
 
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 79a628b..8a7a49c 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
@@ -166,5 +166,15 @@
      * TODO: consider using InterfaceParams to replace interface name.
      */
     public abstract boolean detachProgram(@NonNull String iface);
+
+    /**
+     * Add interface index mapping.
+     */
+    public abstract boolean addDevMap(int ifIndex);
+
+    /**
+     * Remove interface index mapping.
+     */
+    public abstract boolean removeDevMap(int ifIndex);
 }
 
diff --git a/Tethering/bpf_progs/Android.bp b/Tethering/bpf_progs/Android.bp
index 289d75d..5b00dfe 100644
--- a/Tethering/bpf_progs/Android.bp
+++ b/Tethering/bpf_progs/Android.bp
@@ -48,10 +48,6 @@
         "-Wall",
         "-Werror",
     ],
-    include_dirs: [
-        // TODO: get rid of system/netd.
-        "system/netd/bpf_progs",             // for bpf_net_helpers.h
-    ],
 }
 
 bpf {
@@ -61,8 +57,4 @@
         "-Wall",
         "-Werror",
     ],
-    include_dirs: [
-        // TODO: get rid of system/netd.
-        "system/netd/bpf_progs",             // for bpf_net_helpers.h
-    ],
 }
diff --git a/Tethering/bpf_progs/bpf_net_helpers.h b/Tethering/bpf_progs/bpf_net_helpers.h
new file mode 100644
index 0000000..c798580
--- /dev/null
+++ b/Tethering/bpf_progs/bpf_net_helpers.h
@@ -0,0 +1,72 @@
+/*
+ * 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 <linux/bpf.h>
+#include <linux/if_packet.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+// this returns 0 iff skb->sk is NULL
+static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
+
+static uint32_t (*bpf_get_socket_uid)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_uid;
+
+static int (*bpf_skb_pull_data)(struct __sk_buff* skb, __u32 len) = (void*)BPF_FUNC_skb_pull_data;
+
+static int (*bpf_skb_load_bytes)(struct __sk_buff* skb, int off, void* to,
+                                 int len) = (void*)BPF_FUNC_skb_load_bytes;
+
+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 int64_t (*bpf_csum_diff)(__be32* from, __u32 from_size, __be32* to, __u32 to_size,
+                                __wsum seed) = (void*)BPF_FUNC_csum_diff;
+
+static int64_t (*bpf_csum_update)(struct __sk_buff* skb, __wsum csum) = (void*)BPF_FUNC_csum_update;
+
+static int (*bpf_skb_change_proto)(struct __sk_buff* skb, __be16 proto,
+                                   __u64 flags) = (void*)BPF_FUNC_skb_change_proto;
+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 int (*bpf_l4_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
+                                  __u64 flags) = (void*)BPF_FUNC_l4_csum_replace;
+static int (*bpf_redirect)(__u32 ifindex, __u64 flags) = (void*)BPF_FUNC_redirect;
+static int (*bpf_redirect_map)(const struct bpf_map_def* map, __u32 key,
+                               __u64 flags) = (void*)BPF_FUNC_redirect_map;
+
+static int (*bpf_skb_change_head)(struct __sk_buff* skb, __u32 head_room,
+                                  __u64 flags) = (void*)BPF_FUNC_skb_change_head;
+static int (*bpf_skb_adjust_room)(struct __sk_buff* skb, __s32 len_diff, __u32 mode,
+                                  __u64 flags) = (void*)BPF_FUNC_skb_adjust_room;
+
+// Android only supports little endian architectures
+#define htons(x) (__builtin_constant_p(x) ? ___constant_swab16(x) : __builtin_bswap16(x))
+#define htonl(x) (__builtin_constant_p(x) ? ___constant_swab32(x) : __builtin_bswap32(x))
+#define ntohs(x) htons(x)
+#define ntohl(x) htonl(x)
+
+static inline __always_inline __unused bool is_received_skb(struct __sk_buff* skb) {
+    return skb->pkt_type == PACKET_HOST || skb->pkt_type == PACKET_BROADCAST ||
+           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) {
+    if (len > skb->len) len = skb->len;
+    if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
+}
diff --git a/Tethering/bpf_progs/offload.c b/Tethering/bpf_progs/offload.c
index 36f6783..6ff370c 100644
--- a/Tethering/bpf_progs/offload.c
+++ b/Tethering/bpf_progs/offload.c
@@ -767,8 +767,7 @@
 
 // ----- XDP Support -----
 
-DEFINE_BPF_MAP_GRW(tether_xdp_devmap, DEVMAP_HASH, uint32_t, uint32_t, 64,
-                   AID_NETWORK_STACK)
+DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, AID_NETWORK_STACK)
 
 static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const bool is_ethernet,
         const bool downstream) {
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index b141eae..f652772 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -21,16 +21,16 @@
     name: "framework-tethering",
     defaults: ["framework-module-defaults"],
     impl_library_visibility: [
-        "//frameworks/base/packages/Tethering:__subpackages__",
         "//packages/modules/Connectivity/Tethering:__subpackages__",
+        "//packages/modules/Connectivity/tests:__subpackages__",
     ],
 
     srcs: [":framework-tethering-srcs"],
-    libs: ["framework-connectivity"],
-    stub_only_libs: ["framework-connectivity"],
+    libs: ["framework-connectivity.stubs.module_lib"],
+    stub_only_libs: ["framework-connectivity.stubs.module_lib"],
     aidl: {
         include_dirs: [
-            "frameworks/base/packages/Connectivity/framework/aidl-export",
+            "packages/modules/Connectivity/framework/aidl-export",
         ],
     },
 
@@ -57,6 +57,8 @@
         "src/android/net/TetheringConfigurationParcel.aidl",
         "src/android/net/TetheringRequestParcel.aidl",
         "src/android/net/TetherStatesParcel.aidl",
+        "src/android/net/TetheringInterface.aidl",
+        "src/android/net/TetheringInterface.java",
     ],
     path: "src"
 }
diff --git a/Tethering/common/TetheringLib/api/module-lib-current.txt b/Tethering/common/TetheringLib/api/module-lib-current.txt
index 6ddb122..0566040 100644
--- a/Tethering/common/TetheringLib/api/module-lib-current.txt
+++ b/Tethering/common/TetheringLib/api/module-lib-current.txt
@@ -28,13 +28,13 @@
   }
 
   public static interface TetheringManager.TetheringEventCallback {
-    method public default void onTetherableInterfaceRegexpsChanged(@NonNull android.net.TetheringManager.TetheringInterfaceRegexps);
+    method @Deprecated public default void onTetherableInterfaceRegexpsChanged(@NonNull android.net.TetheringManager.TetheringInterfaceRegexps);
   }
 
-  public static class TetheringManager.TetheringInterfaceRegexps {
-    method @NonNull public java.util.List<java.lang.String> getTetherableBluetoothRegexs();
-    method @NonNull public java.util.List<java.lang.String> getTetherableUsbRegexs();
-    method @NonNull public java.util.List<java.lang.String> getTetherableWifiRegexs();
+  @Deprecated public static class TetheringManager.TetheringInterfaceRegexps {
+    method @Deprecated @NonNull public java.util.List<java.lang.String> getTetherableBluetoothRegexs();
+    method @Deprecated @NonNull public java.util.List<java.lang.String> getTetherableUsbRegexs();
+    method @Deprecated @NonNull public java.util.List<java.lang.String> getTetherableWifiRegexs();
   }
 
 }
diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt
index 105bab1..844ff64 100644
--- a/Tethering/common/TetheringLib/api/system-current.txt
+++ b/Tethering/common/TetheringLib/api/system-current.txt
@@ -19,6 +19,15 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheredClient.AddressInfo> CREATOR;
   }
 
+  public final class TetheringInterface implements android.os.Parcelable {
+    ctor public TetheringInterface(int, @NonNull String);
+    method public int describeContents();
+    method @NonNull public String getInterface();
+    method public int getType();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.TetheringInterface> CREATOR;
+  }
+
   public class TetheringManager {
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.TetheringEventCallback);
     method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void requestLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.OnTetheringEntitlementResultListener);
@@ -26,7 +35,7 @@
     method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopAllTethering();
     method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopTethering(int);
     method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.ACCESS_NETWORK_STATE}) public void unregisterTetheringEventCallback(@NonNull android.net.TetheringManager.TetheringEventCallback);
-    field public static final String ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED";
+    field @Deprecated public static final String ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED";
     field public static final int CONNECTIVITY_SCOPE_GLOBAL = 1; // 0x1
     field public static final int CONNECTIVITY_SCOPE_LOCAL = 2; // 0x2
     field public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY";
@@ -74,10 +83,14 @@
   public static interface TetheringManager.TetheringEventCallback {
     method public default void onClientsChanged(@NonNull java.util.Collection<android.net.TetheredClient>);
     method public default void onError(@NonNull String, int);
+    method public default void onError(@NonNull android.net.TetheringInterface, int);
     method public default void onLocalOnlyInterfacesChanged(@NonNull java.util.List<java.lang.String>);
+    method public default void onLocalOnlyInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
     method public default void onOffloadStatusChanged(int);
     method public default void onTetherableInterfacesChanged(@NonNull java.util.List<java.lang.String>);
+    method public default void onTetherableInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
     method public default void onTetheredInterfacesChanged(@NonNull java.util.List<java.lang.String>);
+    method public default void onTetheredInterfacesChanged(@NonNull java.util.Set<android.net.TetheringInterface>);
     method public default void onTetheringSupported(boolean);
     method public default void onUpstreamChanged(@Nullable android.net.Network);
   }
diff --git a/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl
index 3d842b3..43262fb 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl
+++ b/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl
@@ -16,15 +16,17 @@
 
 package android.net;
 
+import android.net.TetheringInterface;
+
 /**
  * Status details for tethering downstream interfaces.
  * {@hide}
  */
 parcelable TetherStatesParcel {
-    String[] availableList;
-    String[] tetheredList;
-    String[] localOnlyList;
-    String[] erroredIfaceList;
+    TetheringInterface[] availableList;
+    TetheringInterface[] tetheredList;
+    TetheringInterface[] localOnlyList;
+    TetheringInterface[] erroredIfaceList;
     // List of Last error code corresponding to each errored iface in erroredIfaceList. */
     // TODO: Improve this as b/143122247.
     int[] lastErrorList;
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.aidl
new file mode 100644
index 0000000..7151984
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.aidl
@@ -0,0 +1,18 @@
+/*
+ * 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;
+
+@JavaOnlyStableParcelable parcelable TetheringInterface;
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java
new file mode 100644
index 0000000..84cdef1
--- /dev/null
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.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.annotation.SystemApi;
+import android.net.TetheringManager.TetheringType;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * The mapping of tethering interface and type.
+ * @hide
+ */
+@SystemApi
+public final class TetheringInterface implements Parcelable {
+    private final int mType;
+    private final String mInterface;
+
+    public TetheringInterface(@TetheringType int type, @NonNull String iface) {
+        Objects.requireNonNull(iface);
+        mType = type;
+        mInterface = iface;
+    }
+
+    private TetheringInterface(@NonNull Parcel in) {
+        this(in.readInt(), in.readString());
+    }
+
+    /** Get tethering type. */
+    public int getType() {
+        return mType;
+    }
+
+    /** Get tethering interface. */
+    @NonNull
+    public String getInterface() {
+        return mInterface;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mType);
+        dest.writeString(mInterface);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mType, mInterface);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof TetheringInterface)) return false;
+        final TetheringInterface other = (TetheringInterface) obj;
+        return mType == other.mType && mInterface.equals(other.mInterface);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Creator<TetheringInterface> CREATOR = new Creator<TetheringInterface>() {
+        @NonNull
+        @Override
+        public TetheringInterface createFromParcel(@NonNull Parcel in) {
+            return new TetheringInterface(in);
+        }
+
+        @NonNull
+        @Override
+        public TetheringInterface[] newArray(int size) {
+            return new TetheringInterface[size];
+        }
+    };
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "TetheringInterface {mType=" + mType
+                + ", mInterface=" + mInterface + "}";
+    }
+}
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index c64da8a..73ab908 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -30,6 +30,7 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
@@ -43,6 +44,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.Supplier;
 
@@ -83,7 +85,10 @@
      * {@code TetheringManager.EXTRA_ERRORED_TETHER} to indicate
      * the current state of tethering.  Each include a list of
      * interface names in that state (may be empty).
+     *
+     * @deprecated New client should use TetheringEventCallback instead.
      */
+    @Deprecated
     public static final String ACTION_TETHER_STATE_CHANGED =
             "android.net.conn.TETHER_STATE_CHANGED";
 
@@ -531,7 +536,7 @@
     /**
      * Attempt to both alter the mode of USB and Tethering of USB.
      *
-     * @deprecated New client should not use this API anymore. All clients should use
+     * @deprecated New clients should not use this API anymore. All clients should use
      * #startTethering or #stopTethering which encapsulate proper entitlement logic. If the API is
      * used and an entitlement check is needed, downstream USB tethering will be enabled but will
      * not have any upstream.
@@ -978,8 +983,13 @@
          * multiple times later upon changes.
          * @param reg The new regular expressions.
          *
+         * @deprecated New clients should use the callbacks with {@link TetheringInterface} which
+         * has the mapping between tethering type and interface. InterfaceRegex is no longer needed
+         * to determine the mapping of tethering type and interface.
+         *
          * @hide
          */
+        @Deprecated
         @SystemApi(client = MODULE_LIBRARIES)
         default void onTetherableInterfaceRegexpsChanged(@NonNull TetheringInterfaceRegexps reg) {}
 
@@ -994,15 +1004,43 @@
         default void onTetherableInterfacesChanged(@NonNull List<String> interfaces) {}
 
         /**
+         * Called when there was a change in the list of tetherable interfaces. Tetherable
+         * interface means this interface is available and can be used for tethering.
+         *
+         * <p>This will be called immediately after the callback is registered, and may be called
+         * multiple times later upon changes.
+         * @param interfaces The set of TetheringInterface of currently tetherable interface.
+         */
+        default void onTetherableInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
+            // By default, the new callback calls the old callback, so apps
+            // implementing the old callback just work.
+            onTetherableInterfacesChanged(toIfaces(interfaces));
+        }
+
+        /**
          * Called when there was a change in the list of tethered interfaces.
          *
          * <p>This will be called immediately after the callback is registered, and may be called
          * multiple times later upon changes.
-         * @param interfaces The list of 0 or more String of currently tethered interface names.
+         * @param interfaces The lit of 0 or more String of currently tethered interface names.
          */
         default void onTetheredInterfacesChanged(@NonNull List<String> interfaces) {}
 
         /**
+         * Called when there was a change in the list of tethered interfaces.
+         *
+         * <p>This will be called immediately after the callback is registered, and may be called
+         * multiple times later upon changes.
+         * @param interfaces The set of 0 or more TetheringInterface of currently tethered
+         * interface.
+         */
+        default void onTetheredInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
+            // By default, the new callback calls the old callback, so apps
+            // implementing the old callback just work.
+            onTetheredInterfacesChanged(toIfaces(interfaces));
+        }
+
+        /**
          * Called when there was a change in the list of local-only interfaces.
          *
          * <p>This will be called immediately after the callback is registered, and may be called
@@ -1012,6 +1050,20 @@
         default void onLocalOnlyInterfacesChanged(@NonNull List<String> interfaces) {}
 
         /**
+         * Called when there was a change in the list of local-only interfaces.
+         *
+         * <p>This will be called immediately after the callback is registered, and may be called
+         * multiple times later upon changes.
+         * @param interfaces The set of 0 or more TetheringInterface of active local-only
+         * interface.
+         */
+        default void onLocalOnlyInterfacesChanged(@NonNull Set<TetheringInterface> interfaces) {
+            // By default, the new callback calls the old callback, so apps
+            // implementing the old callback just work.
+            onLocalOnlyInterfacesChanged(toIfaces(interfaces));
+        }
+
+        /**
          * Called when an error occurred configuring tethering.
          *
          * <p>This will be called immediately after the callback is registered if the latest status
@@ -1022,6 +1074,20 @@
         default void onError(@NonNull String ifName, @TetheringIfaceError int error) {}
 
         /**
+         * Called when an error occurred configuring tethering.
+         *
+         * <p>This will be called immediately after the callback is registered if the latest status
+         * on the interface is an error, and may be called multiple times later upon changes.
+         * @param iface The interface that experienced the error.
+         * @param error One of {@code TetheringManager#TETHER_ERROR_*}.
+         */
+        default void onError(@NonNull TetheringInterface iface, @TetheringIfaceError int error) {
+            // By default, the new callback calls the old callback, so apps
+            // implementing the old callback just work.
+            onError(iface.getInterface(), error);
+        }
+
+        /**
          * Called when the list of tethered clients changes.
          *
          * <p>This callback provides best-effort information on connected clients based on state
@@ -1044,9 +1110,37 @@
     }
 
     /**
-     * Regular expressions used to identify tethering interfaces.
+     * Covert DownStreamInterface collection to interface String array list. Internal use only.
+     *
      * @hide
      */
+    public static ArrayList<String> toIfaces(Collection<TetheringInterface> tetherIfaces) {
+        final ArrayList<String> ifaces = new ArrayList<>();
+        for (TetheringInterface tether : tetherIfaces) {
+            ifaces.add(tether.getInterface());
+        }
+
+        return ifaces;
+    }
+
+    private static String[] toIfaces(TetheringInterface[] tetherIfaces) {
+        final String[] ifaces = new String[tetherIfaces.length];
+        for (int i = 0; i < tetherIfaces.length; i++) {
+            ifaces[i] = tetherIfaces[i].getInterface();
+        }
+
+        return ifaces;
+    }
+
+
+    /**
+     * Regular expressions used to identify tethering interfaces.
+     *
+     * @deprecated Instead of using regex to determine tethering type. New client could use the
+     * callbacks with {@link TetheringInterface} which has the mapping of type and interface.
+     * @hide
+     */
+    @Deprecated
     @SystemApi(client = MODULE_LIBRARIES)
     public static class TetheringInterfaceRegexps {
         private final String[] mTetherableBluetoothRegexs;
@@ -1114,10 +1208,10 @@
             }
             final ITetheringEventCallback remoteCallback = new ITetheringEventCallback.Stub() {
                 // Only accessed with a lock on this object
-                private final HashMap<String, Integer> mErrorStates = new HashMap<>();
-                private String[] mLastTetherableInterfaces = null;
-                private String[] mLastTetheredInterfaces = null;
-                private String[] mLastLocalOnlyInterfaces = null;
+                private final HashMap<TetheringInterface, Integer> mErrorStates = new HashMap<>();
+                private TetheringInterface[] mLastTetherableInterfaces = null;
+                private TetheringInterface[] mLastTetheredInterfaces = null;
+                private TetheringInterface[] mLastLocalOnlyInterfaces = null;
 
                 @Override
                 public void onUpstreamChanged(Network network) throws RemoteException {
@@ -1128,14 +1222,14 @@
 
                 private synchronized void sendErrorCallbacks(final TetherStatesParcel newStates) {
                     for (int i = 0; i < newStates.erroredIfaceList.length; i++) {
-                        final String iface = newStates.erroredIfaceList[i];
-                        final Integer lastError = mErrorStates.get(iface);
+                        final TetheringInterface tetherIface = newStates.erroredIfaceList[i];
+                        final Integer lastError = mErrorStates.get(tetherIface);
                         final int newError = newStates.lastErrorList[i];
                         if (newError != TETHER_ERROR_NO_ERROR
                                 && !Objects.equals(lastError, newError)) {
-                            callback.onError(iface, newError);
+                            callback.onError(tetherIface, newError);
                         }
-                        mErrorStates.put(iface, newError);
+                        mErrorStates.put(tetherIface, newError);
                     }
                 }
 
@@ -1144,7 +1238,7 @@
                     if (Arrays.equals(mLastTetherableInterfaces, newStates.availableList)) return;
                     mLastTetherableInterfaces = newStates.availableList.clone();
                     callback.onTetherableInterfacesChanged(
-                            Collections.unmodifiableList(Arrays.asList(mLastTetherableInterfaces)));
+                            Collections.unmodifiableSet((new ArraySet(mLastTetherableInterfaces))));
                 }
 
                 private synchronized void maybeSendTetheredIfacesChangedCallback(
@@ -1152,7 +1246,7 @@
                     if (Arrays.equals(mLastTetheredInterfaces, newStates.tetheredList)) return;
                     mLastTetheredInterfaces = newStates.tetheredList.clone();
                     callback.onTetheredInterfacesChanged(
-                            Collections.unmodifiableList(Arrays.asList(mLastTetheredInterfaces)));
+                            Collections.unmodifiableSet((new ArraySet(mLastTetheredInterfaces))));
                 }
 
                 private synchronized void maybeSendLocalOnlyIfacesChangedCallback(
@@ -1160,7 +1254,7 @@
                     if (Arrays.equals(mLastLocalOnlyInterfaces, newStates.localOnlyList)) return;
                     mLastLocalOnlyInterfaces = newStates.localOnlyList.clone();
                     callback.onLocalOnlyInterfacesChanged(
-                            Collections.unmodifiableList(Arrays.asList(mLastLocalOnlyInterfaces)));
+                            Collections.unmodifiableSet((new ArraySet(mLastLocalOnlyInterfaces))));
                 }
 
                 // Called immediately after the callbacks are registered.
@@ -1262,8 +1356,8 @@
         if (mTetherStatesParcel == null) return TETHER_ERROR_NO_ERROR;
 
         int i = 0;
-        for (String errored : mTetherStatesParcel.erroredIfaceList) {
-            if (iface.equals(errored)) return mTetherStatesParcel.lastErrorList[i];
+        for (TetheringInterface errored : mTetherStatesParcel.erroredIfaceList) {
+            if (iface.equals(errored.getInterface())) return mTetherStatesParcel.lastErrorList[i];
 
             i++;
         }
@@ -1327,7 +1421,7 @@
         mCallback.waitForStarted();
         if (mTetherStatesParcel == null) return new String[0];
 
-        return mTetherStatesParcel.availableList;
+        return toIfaces(mTetherStatesParcel.availableList);
     }
 
     /**
@@ -1341,7 +1435,7 @@
         mCallback.waitForStarted();
         if (mTetherStatesParcel == null) return new String[0];
 
-        return mTetherStatesParcel.tetheredList;
+        return toIfaces(mTetherStatesParcel.tetheredList);
     }
 
     /**
@@ -1361,7 +1455,7 @@
         mCallback.waitForStarted();
         if (mTetherStatesParcel == null) return new String[0];
 
-        return mTetherStatesParcel.erroredIfaceList;
+        return toIfaces(mTetherStatesParcel.erroredIfaceList);
     }
 
     /**
diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp
index 308dfb9..2fb5985 100644
--- a/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp
+++ b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp
@@ -36,6 +36,9 @@
 // The maximum length of TCA_BPF_NAME. Sync from net/sched/cls_bpf.c.
 #define CLS_BPF_NAME_LEN 256
 
+// Classifier name. See cls_bpf_ops in net/sched/cls_bpf.c.
+#define CLS_BPF_KIND_NAME "bpf"
+
 namespace android {
 // Sync from system/netd/server/NetlinkCommands.h
 const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
@@ -180,7 +183,7 @@
             return false;
         default:
             jniThrowExceptionFmt(env, "java/io/IOException",
-                                 "Unknown hardware address type %s on interface %s", rv,
+                                 "Unknown hardware address type %d on interface %s", rv,
                                  interface.c_str());
             return false;
     }
@@ -205,9 +208,9 @@
         tcmsg t;
         struct {
             nlattr attr;
-            // The maximum classifier name length is defined as IFNAMSIZ.
-            // See tcf_proto_ops in include/net/sch_generic.h.
-            char str[NLMSG_ALIGN(IFNAMSIZ)];
+            // The maximum classifier name length is defined in
+            // tcf_proto_ops in include/net/sch_generic.h.
+            char str[NLMSG_ALIGN(sizeof(CLS_BPF_KIND_NAME))];
         } kind;
         struct {
             nlattr attr;
@@ -248,8 +251,7 @@
                                             .nla_len = sizeof(req.kind),
                                             .nla_type = TCA_KIND,
                                     },
-                            // Classifier name. See cls_bpf_ops in net/sched/cls_bpf.c.
-                            .str = "bpf",
+                            .str = CLS_BPF_KIND_NAME,
                     },
             .options =
                     {
diff --git a/Tethering/res/values/config.xml b/Tethering/res/values/config.xml
index 4391006..0412a49 100644
--- a/Tethering/res/values/config.xml
+++ b/Tethering/res/values/config.xml
@@ -193,4 +193,8 @@
          state. -->
     <!-- Config for showing upstream roaming notification. -->
     <bool name="config_upstream_roaming_notification">false</bool>
+
+    <!-- Which USB function should be enabled when TETHERING_USB is requested. 0: RNDIS, 1: NCM.
+         -->
+    <integer translatable="false" name="config_tether_usb_functions">0</integer>
 </resources>
diff --git a/Tethering/res/values/overlayable.xml b/Tethering/res/values/overlayable.xml
index 0ee7a99..91fbd7d 100644
--- a/Tethering/res/values/overlayable.xml
+++ b/Tethering/res/values/overlayable.xml
@@ -24,6 +24,7 @@
             <item type="array" name="config_tether_wifi_p2p_regexs"/>
             <item type="array" name="config_tether_bluetooth_regexs"/>
             <item type="array" name="config_tether_dhcp_range"/>
+            <item type="integer" name="config_tether_usb_functions"/>
             <!-- Use the BPF offload for tethering when the kernel has support. True by default.
                  If the device doesn't want to support tether BPF offload, this should be false.
                  Note that this setting could be overridden by device config.
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index c45ce83..3428c1d 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -1054,8 +1054,16 @@
         mLastRaParams = newParams;
     }
 
-    private void logMessage(State state, int what) {
-        mLog.log(state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what)));
+    private void maybeLogMessage(State state, int what) {
+        switch (what) {
+            // Suppress some CMD_* to avoid log flooding.
+            case CMD_IPV6_TETHER_UPDATE:
+            case CMD_NEIGHBOR_EVENT:
+                break;
+            default:
+                mLog.log(state.getName() + " got "
+                        + sMagicDecoderRing.get(what, Integer.toString(what)));
+        }
     }
 
     private void sendInterfaceState(int newInterfaceState) {
@@ -1095,7 +1103,7 @@
 
         @Override
         public boolean processMessage(Message message) {
-            logMessage(this, message.what);
+            maybeLogMessage(this, message.what);
             switch (message.what) {
                 case CMD_TETHER_REQUESTED:
                     mLastError = TetheringManager.TETHER_ERROR_NO_ERROR;
@@ -1180,7 +1188,6 @@
 
         @Override
         public boolean processMessage(Message message) {
-            logMessage(this, message.what);
             switch (message.what) {
                 case CMD_TETHER_UNREQUESTED:
                     transitionTo(mInitialState);
@@ -1238,7 +1245,7 @@
         public boolean processMessage(Message message) {
             if (super.processMessage(message)) return true;
 
-            logMessage(this, message.what);
+            maybeLogMessage(this, message.what);
             switch (message.what) {
                 case CMD_TETHER_REQUESTED:
                     mLog.e("CMD_TETHER_REQUESTED while in local-only hotspot mode.");
@@ -1306,7 +1313,7 @@
         public boolean processMessage(Message message) {
             if (super.processMessage(message)) return true;
 
-            logMessage(this, message.what);
+            maybeLogMessage(this, message.what);
             switch (message.what) {
                 case CMD_TETHER_REQUESTED:
                     mLog.e("CMD_TETHER_REQUESTED while already tethering.");
@@ -1417,7 +1424,7 @@
     class WaitingForRestartState extends State {
         @Override
         public boolean processMessage(Message message) {
-            logMessage(this, message.what);
+            maybeLogMessage(this, message.what);
             switch (message.what) {
                 case CMD_TETHER_UNREQUESTED:
                     transitionTo(mInitialState);
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index add4f37..4a05c9f 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -47,8 +47,8 @@
 import android.net.util.InterfaceParams;
 import android.net.util.SharedLog;
 import android.net.util.TetheringUtils.ForwardedStats;
-import android.os.ConditionVariable;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.text.TextUtils;
 import android.util.Log;
@@ -73,6 +73,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -105,6 +106,7 @@
     private static final String TETHER_STATS_MAP_PATH = makeMapPath("stats");
     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");
 
     /** The names of all the BPF counters defined in bpf_tethering.h. */
     public static final String[] sBpfCounterNames = getBpfCounterNames();
@@ -221,6 +223,11 @@
     // Map for upstream and downstream pair.
     private final HashMap<String, HashSet<String>> mForwardingPairs = new HashMap<>();
 
+    // Set for upstream and downstream device map. Used for caching BPF dev map status and
+    // reduce duplicate adding or removing map operations. Use LinkedHashSet because the test
+    // BpfCoordinatorTest needs predictable iteration order.
+    private final Set<Integer> mDeviceMapSet = new LinkedHashSet<>();
+
     // Runnable that used by scheduling next polling of stats.
     private final Runnable mScheduledPollingTask = () -> {
         updateForwardedStats();
@@ -337,6 +344,18 @@
                 return null;
             }
         }
+
+        /** Get dev BPF map. */
+        @Nullable public BpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() {
+            if (!isAtLeastS()) return null;
+            try {
+                return new BpfMap<>(TETHER_DEV_MAP_PATH,
+                    BpfMap.BPF_F_RDWR, TetherDevKey.class, TetherDevValue.class);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot create dev map: " + e);
+                return null;
+            }
+        }
     }
 
     @VisibleForTesting
@@ -491,6 +510,9 @@
         }
         LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(ipServer);
 
+        // Add upstream and downstream interface index to dev map.
+        maybeAddDevMap(rule.upstreamIfindex, rule.downstreamIfindex);
+
         // When the first rule is added to an upstream, setup upstream forwarding and data limit.
         maybeSetLimit(rule.upstreamIfindex);
 
@@ -735,43 +757,47 @@
      * be allowed to be accessed on the handler thread.
      */
     public void dump(@NonNull IndentingPrintWriter pw) {
-        final ConditionVariable dumpDone = new ConditionVariable();
-        mHandler.post(() -> {
-            pw.println("mIsBpfEnabled: " + mIsBpfEnabled);
-            pw.println("Polling " + (mPollingStarted ? "started" : "not started"));
-            pw.println("Stats provider " + (mStatsProvider != null
-                    ? "registered" : "not registered"));
-            pw.println("Upstream quota: " + mInterfaceQuotas.toString());
-            pw.println("Polling interval: " + getPollingInterval() + " ms");
-            pw.println("Bpf shim: " + mBpfCoordinatorShim.toString());
+        pw.println("mIsBpfEnabled: " + mIsBpfEnabled);
+        pw.println("Polling " + (mPollingStarted ? "started" : "not started"));
+        pw.println("Stats provider " + (mStatsProvider != null
+                ? "registered" : "not registered"));
+        pw.println("Upstream quota: " + mInterfaceQuotas.toString());
+        pw.println("Polling interval: " + getPollingInterval() + " ms");
+        pw.println("Bpf shim: " + mBpfCoordinatorShim.toString());
 
-            pw.println("Forwarding stats:");
-            pw.increaseIndent();
-            if (mStats.size() == 0) {
-                pw.println("<empty>");
-            } else {
-                dumpStats(pw);
-            }
-            pw.decreaseIndent();
-
-            pw.println("Forwarding rules:");
-            pw.increaseIndent();
-            dumpIpv6UpstreamRules(pw);
-            dumpIpv6ForwardingRules(pw);
-            dumpIpv4ForwardingRules(pw);
-            pw.decreaseIndent();
-
-            pw.println();
-            pw.println("Forwarding counters:");
-            pw.increaseIndent();
-            dumpCounters(pw);
-            pw.decreaseIndent();
-
-            dumpDone.open();
-        });
-        if (!dumpDone.block(DUMP_TIMEOUT_MS)) {
-            pw.println("... dump timed-out after " + DUMP_TIMEOUT_MS + "ms");
+        pw.println("Forwarding stats:");
+        pw.increaseIndent();
+        if (mStats.size() == 0) {
+            pw.println("<empty>");
+        } else {
+            dumpStats(pw);
         }
+        pw.decreaseIndent();
+
+        pw.println("BPF stats:");
+        pw.increaseIndent();
+        dumpBpfStats(pw);
+        pw.decreaseIndent();
+        pw.println();
+
+        pw.println("Forwarding rules:");
+        pw.increaseIndent();
+        dumpIpv6UpstreamRules(pw);
+        dumpIpv6ForwardingRules(pw);
+        dumpIpv4ForwardingRules(pw);
+        pw.decreaseIndent();
+        pw.println();
+
+        pw.println("Device map:");
+        pw.increaseIndent();
+        dumpDevmap(pw);
+        pw.decreaseIndent();
+
+        pw.println();
+        pw.println("Forwarding counters:");
+        pw.increaseIndent();
+        dumpCounters(pw);
+        pw.decreaseIndent();
     }
 
     private void dumpStats(@NonNull IndentingPrintWriter pw) {
@@ -782,6 +808,22 @@
                     upstreamIfindex), stats.toString()));
         }
     }
+    private void dumpBpfStats(@NonNull IndentingPrintWriter pw) {
+        try (BpfMap<TetherStatsKey, TetherStatsValue> map = mDeps.getBpfStatsMap()) {
+            if (map == null) {
+                pw.println("No BPF stats map");
+                return;
+            }
+            if (map.isEmpty()) {
+                pw.println("<empty>");
+            }
+            map.forEach((k, v) -> {
+                pw.println(String.format("%s: %s", k, v));
+            });
+        } catch (ErrnoException e) {
+            pw.println("Error dumping BPF stats map: " + e);
+        }
+    }
 
     private void dumpIpv6ForwardingRules(@NonNull IndentingPrintWriter pw) {
         if (mIpv6ForwardingRules.size() == 0) {
@@ -831,38 +873,61 @@
         }
     }
 
-    private String ipv4RuleToString(Tether4Key key, Tether4Value value) {
-        final String private4, public4, dst4;
+    private String ipv4RuleToString(long now, boolean downstream,
+            Tether4Key key, Tether4Value value) {
+        final String src4, public4, dst4;
+        final int publicPort;
         try {
-            private4 = InetAddress.getByAddress(key.src4).getHostAddress();
-            dst4 = InetAddress.getByAddress(key.dst4).getHostAddress();
-            public4 = InetAddress.getByAddress(value.src46).getHostAddress();
+            src4 = InetAddress.getByAddress(key.src4).getHostAddress();
+            if (downstream) {
+                public4 = InetAddress.getByAddress(key.dst4).getHostAddress();
+                publicPort = key.dstPort;
+            } else {
+                public4 = InetAddress.getByAddress(value.src46).getHostAddress();
+                publicPort = value.srcPort;
+            }
+            dst4 = InetAddress.getByAddress(value.dst46).getHostAddress();
         } catch (UnknownHostException impossible) {
-            throw new AssertionError("4-byte array not valid IPv4 address!");
+            throw new AssertionError("IP address array not valid IPv4 address!");
         }
-        return String.format("[%s] %d(%s) %s:%d -> %d(%s) %s:%d -> %s:%d",
-                key.dstMac, key.iif, getIfName(key.iif), private4, key.srcPort,
+
+        final long ageMs = (now - value.lastUsed) / 1_000_000;
+        return String.format("[%s] %d(%s) %s:%d -> %d(%s) %s:%d -> %s:%d [%s] %dms",
+                key.dstMac, key.iif, getIfName(key.iif), src4, key.srcPort,
                 value.oif, getIfName(value.oif),
-                public4, value.srcPort, dst4, key.dstPort);
+                public4, publicPort, dst4, value.dstPort, value.ethDstMac, ageMs);
+    }
+
+    private void dumpIpv4ForwardingRuleMap(long now, boolean downstream,
+            BpfMap<Tether4Key, Tether4Value> map, IndentingPrintWriter pw) throws ErrnoException {
+        if (map == null) {
+            pw.println("No IPv4 support");
+            return;
+        }
+        if (map.isEmpty()) {
+            pw.println("No rules");
+            return;
+        }
+        map.forEach((k, v) -> pw.println(ipv4RuleToString(now, downstream, k, v)));
     }
 
     private void dumpIpv4ForwardingRules(IndentingPrintWriter pw) {
-        try (BpfMap<Tether4Key, Tether4Value> map = mDeps.getBpfUpstream4Map()) {
-            if (map == null) {
-                pw.println("No IPv4 support");
-                return;
-            }
-            if (map.isEmpty()) {
-                pw.println("No IPv4 rules");
-                return;
-            }
-            pw.println("IPv4: [inDstMac] iif(iface) src -> nat -> dst");
+        final long now = SystemClock.elapsedRealtimeNanos();
+
+        try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map();
+                BpfMap<Tether4Key, Tether4Value> downstreamMap = mDeps.getBpfDownstream4Map()) {
+            pw.println("IPv4 Upstream: [inDstMac] iif(iface) src -> nat -> dst [outDstMac] age");
             pw.increaseIndent();
-            map.forEach((k, v) -> pw.println(ipv4RuleToString(k, v)));
+            dumpIpv4ForwardingRuleMap(now, UPSTREAM, upstreamMap, pw);
+            pw.decreaseIndent();
+
+            pw.println("IPv4 Downstream: [inDstMac] iif(iface) src -> nat -> dst [outDstMac] age");
+            pw.increaseIndent();
+            dumpIpv4ForwardingRuleMap(now, DOWNSTREAM, downstreamMap, pw);
+            pw.decreaseIndent();
         } catch (ErrnoException e) {
             pw.println("Error dumping IPv4 map: " + e);
         }
-        pw.decreaseIndent();
     }
 
     /**
@@ -899,6 +964,31 @@
         }
     }
 
+    private void dumpDevmap(@NonNull IndentingPrintWriter pw) {
+        try (BpfMap<TetherDevKey, TetherDevValue> map = mDeps.getBpfDevMap()) {
+            if (map == null) {
+                pw.println("No devmap support");
+                return;
+            }
+            if (map.isEmpty()) {
+                pw.println("No interface index");
+                return;
+            }
+            pw.println("ifindex (iface) -> ifindex (iface)");
+            pw.increaseIndent();
+            map.forEach((k, v) -> {
+                // Only get upstream interface name. Just do the best to make the index readable.
+                // TODO: get downstream interface name because the index is either upstrema or
+                // downstream interface in dev map.
+                pw.println(String.format("%d (%s) -> %d (%s)", k.ifIndex, getIfName(k.ifIndex),
+                        v.ifIndex, getIfName(v.ifIndex)));
+            });
+        } catch (ErrnoException e) {
+            pw.println("Error dumping dev map: " + e);
+        }
+        pw.decreaseIndent();
+    }
+
     /** IPv6 forwarding rule class. */
     public static class Ipv6ForwardingRule {
         // The upstream6 and downstream6 rules are built as the following tables. Only raw ip
@@ -1238,6 +1328,7 @@
             final Tether4Value downstream4Value = makeTetherDownstream4Value(e, tetherClient,
                     upstreamIndex);
 
+            maybeAddDevMap(upstreamIndex, tetherClient.downstreamIfindex);
             maybeSetLimit(upstreamIndex);
             mBpfCoordinatorShim.tetherOffloadRuleAdd(UPSTREAM, upstream4Key, upstream4Value);
             mBpfCoordinatorShim.tetherOffloadRuleAdd(DOWNSTREAM, downstream4Key, downstream4Value);
@@ -1366,6 +1457,15 @@
         return false;
     }
 
+    // TODO: remove the index from map while the interface has been removed because the map size
+    // is 64 entries. See packages\modules\Connectivity\Tethering\bpf_progs\offload.c.
+    private void maybeAddDevMap(int upstreamIfindex, int downstreamIfindex) {
+        for (Integer index : new Integer[] {upstreamIfindex, downstreamIfindex}) {
+            if (mDeviceMapSet.contains(index)) continue;
+            if (mBpfCoordinatorShim.addDevMap(index)) mDeviceMapSet.add(index);
+        }
+    }
+
     private void forwardingPairAdd(@NonNull String intIface, @NonNull String extIface) {
         if (!mForwardingPairs.containsKey(extIface)) {
             mForwardingPairs.put(extIface, new HashSet<String>());
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index b1e3cfe..60fcfd0 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -41,7 +41,6 @@
 import android.content.IntentFilter;
 import android.net.util.SharedLog;
 import android.os.Bundle;
-import android.os.ConditionVariable;
 import android.os.Handler;
 import android.os.Parcel;
 import android.os.PersistableBundle;
@@ -516,25 +515,18 @@
      * @param pw {@link PrintWriter} is used to print formatted
      */
     public void dump(PrintWriter pw) {
-        final ConditionVariable mWaiting = new ConditionVariable();
-        mHandler.post(() -> {
-            pw.print("isCellularUpstreamPermitted: ");
-            pw.println(isCellularUpstreamPermitted());
-            for (int type = mCurrentDownstreams.nextSetBit(0); type >= 0;
-                    type = mCurrentDownstreams.nextSetBit(type + 1)) {
-                pw.print("Type: ");
-                pw.print(typeString(type));
-                if (mCurrentEntitlementResults.indexOfKey(type) > -1) {
-                    pw.print(", Value: ");
-                    pw.println(errorString(mCurrentEntitlementResults.get(type)));
-                } else {
-                    pw.println(", Value: empty");
-                }
+        pw.print("isCellularUpstreamPermitted: ");
+        pw.println(isCellularUpstreamPermitted());
+        for (int type = mCurrentDownstreams.nextSetBit(0); type >= 0;
+                type = mCurrentDownstreams.nextSetBit(type + 1)) {
+            pw.print("Type: ");
+            pw.print(typeString(type));
+            if (mCurrentEntitlementResults.indexOfKey(type) > -1) {
+                pw.print(", Value: ");
+                pw.println(errorString(mCurrentEntitlementResults.get(type)));
+            } else {
+                pw.println(", Value: empty");
             }
-            mWaiting.open();
-        });
-        if (!mWaiting.block(DUMP_TIMEOUT)) {
-            pw.println("... dump timed out after " + DUMP_TIMEOUT + "ms");
         }
         pw.print("Exempted: [");
         for (int type = mExemptedDownstreams.nextSetBit(0); type >= 0;
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherDevKey.java b/Tethering/src/com/android/networkstack/tethering/TetherDevKey.java
new file mode 100644
index 0000000..4283c1b
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherDevKey.java
@@ -0,0 +1,31 @@
+/*
+ * 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.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 mapping interface index. */
+public class TetherDevKey extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long ifIndex;  // interface index
+
+    public TetherDevKey(final long ifIndex) {
+        this.ifIndex = ifIndex;
+    }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherDevValue.java b/Tethering/src/com/android/networkstack/tethering/TetherDevValue.java
new file mode 100644
index 0000000..1cd99b5
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherDevValue.java
@@ -0,0 +1,31 @@
+/*
+ * 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.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 mapping interface index. */
+public class TetherDevValue extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long ifIndex;  // interface index
+
+    public TetherDevValue(final long ifIndex) {
+        this.ifIndex = ifIndex;
+    }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 8db4cb9..b52ec86 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -51,6 +51,7 @@
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
+import static android.net.TetheringManager.toIfaces;
 import static android.net.util.TetheringMessageBase.BASE_MAIN_SM;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
@@ -63,6 +64,7 @@
 import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_FORCE_USB_FUNCTIONS;
 import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
 import static com.android.networkstack.tethering.UpstreamNetworkMonitor.isCellular;
 
@@ -76,6 +78,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.database.ContentObserver;
 import android.hardware.usb.UsbManager;
 import android.net.ConnectivityManager;
 import android.net.EthernetManager;
@@ -92,6 +95,7 @@
 import android.net.TetheredClient;
 import android.net.TetheringCallbackStartedParcel;
 import android.net.TetheringConfigurationParcel;
+import android.net.TetheringInterface;
 import android.net.TetheringManager.TetheringRequest;
 import android.net.TetheringRequestParcel;
 import android.net.ip.IpServer;
@@ -128,7 +132,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.MessageUtils;
@@ -144,11 +147,13 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  *
@@ -166,6 +171,9 @@
     };
     private static final SparseArray<String> sMagicDecoderRing =
             MessageUtils.findMessageNames(sMessageClasses);
+
+    private static final int DUMP_TIMEOUT_MS = 10_000;
+
     // Keep in sync with NETID_UNSET in system/netd/include/netid_client.h
     private static final int NETID_UNSET = 0;
 
@@ -212,9 +220,6 @@
     private final SparseArray<TetheringRequestParcel> mActiveTetheringRequests =
             new SparseArray<>();
 
-    // used to synchronize public access to members
-    // TODO(b/153621704): remove mPublicSync to make Tethering lock free
-    private final Object mPublicSync;
     private final Context mContext;
     private final ArrayMap<String, TetherState> mTetherStates;
     private final BroadcastReceiver mStateReceiver;
@@ -240,13 +245,12 @@
     private final BpfCoordinator mBpfCoordinator;
     private final PrivateAddressCoordinator mPrivateAddressCoordinator;
     private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
-    // All the usage of mTetheringEventCallback should run in the same thread.
-    private ITetheringEventCallback mTetheringEventCallback = null;
 
     private volatile TetheringConfiguration mConfig;
     private InterfaceSet mCurrentUpstreamIfaceSet;
 
     private boolean mRndisEnabled;       // track the RNDIS function enabled state
+    private boolean mNcmEnabled;         // track the NCM function enabled state
     // True iff. WiFi tethering should be started when soft AP is ready.
     private boolean mWifiTetherRequested;
     private Network mTetherUpstream;
@@ -255,12 +259,10 @@
     private String mWifiP2pTetherInterface = null;
     private int mOffloadStatus = TETHER_HARDWARE_OFFLOAD_STOPPED;
 
-    @GuardedBy("mPublicSync")
     private EthernetManager.TetheredInterfaceRequest mEthernetIfaceRequest;
-    @GuardedBy("mPublicSync")
     private String mConfiguredEthernetIface;
-    @GuardedBy("mPublicSync")
     private EthernetCallback mEthernetCallback;
+    private SettingsObserver mSettingsObserver;
 
     public Tethering(TetheringDependencies deps) {
         mLog.mark("Tethering.constructed");
@@ -270,8 +272,6 @@
         mLooper = mDeps.getTetheringLooper();
         mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
 
-        mPublicSync = new Object();
-
         mTetherStates = new ArrayMap<>();
         mConnectedClientsTracker = new ConnectedClientsTracker();
 
@@ -314,6 +314,10 @@
                     mEntitlementMgr.reevaluateSimCardProvisioning(mConfig);
                 });
 
+        mSettingsObserver = new SettingsObserver(mHandler);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(TETHER_FORCE_USB_FUNCTIONS), false, mSettingsObserver);
+
         mStateReceiver = new StateReceiver();
 
         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
@@ -363,6 +367,28 @@
         startStateMachineUpdaters();
     }
 
+    private class SettingsObserver extends ContentObserver {
+        SettingsObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mLog.i("OBSERVED Settings change");
+            final boolean isUsingNcm = mConfig.isUsingNcm();
+            updateConfiguration();
+            if (isUsingNcm != mConfig.isUsingNcm()) {
+                stopTetheringInternal(TETHERING_USB);
+                stopTetheringInternal(TETHERING_NCM);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    ContentObserver getSettingsObserverForTest() {
+        return mSettingsObserver;
+    }
+
     /**
      * Start to register callbacks.
      * Call this function when tethering is ready to handle callback events.
@@ -494,26 +520,35 @@
         }
     }
 
+    // This method needs to exist because TETHERING_BLUETOOTH and TETHERING_WIGIG can't use
+    // enableIpServing.
+    private void startOrStopIpServer(final String iface, boolean enabled) {
+        // TODO: do not listen to USB interface state changes. USB tethering is driven only by
+        // USB_ACTION broadcasts.
+
+        if (enabled) {
+            ensureIpServerStarted(iface);
+        } else {
+            ensureIpServerStopped(iface);
+        }
+    }
+
     void interfaceStatusChanged(String iface, boolean up) {
         // Never called directly: only called from interfaceLinkStateChanged.
         // See NetlinkHandler.cpp: notifyInterfaceChanged.
         if (VDBG) Log.d(TAG, "interfaceStatusChanged " + iface + ", " + up);
-        synchronized (mPublicSync) {
-            if (up) {
-                maybeTrackNewInterfaceLocked(iface);
-            } else {
-                if (ifaceNameToType(iface) == TETHERING_BLUETOOTH
-                        || ifaceNameToType(iface) == TETHERING_WIGIG) {
-                    stopTrackingInterfaceLocked(iface);
-                } else {
-                    // Ignore usb0 down after enabling RNDIS.
-                    // We will handle disconnect in interfaceRemoved.
-                    // Similarly, ignore interface down for WiFi.  We monitor WiFi AP status
-                    // through the WifiManager.WIFI_AP_STATE_CHANGED_ACTION intent.
-                    if (VDBG) Log.d(TAG, "ignore interface down for " + iface);
-                }
-            }
+
+        final int type = ifaceNameToType(iface);
+        if (!up && type != TETHERING_BLUETOOTH && type != TETHERING_WIGIG) {
+            // Ignore usb interface down after enabling RNDIS.
+            // We will handle disconnect in interfaceRemoved.
+            // Similarly, ignore interface down for WiFi.  We monitor WiFi AP status
+            // through the WifiManager.WIFI_AP_STATE_CHANGED_ACTION intent.
+            if (VDBG) Log.d(TAG, "ignore interface down for " + iface);
+            return;
         }
+
+        startOrStopIpServer(iface, up);
     }
 
     void interfaceLinkStateChanged(String iface, boolean up) {
@@ -541,16 +576,12 @@
 
     void interfaceAdded(String iface) {
         if (VDBG) Log.d(TAG, "interfaceAdded " + iface);
-        synchronized (mPublicSync) {
-            maybeTrackNewInterfaceLocked(iface);
-        }
+        startOrStopIpServer(iface, true /* enabled */);
     }
 
     void interfaceRemoved(String iface) {
         if (VDBG) Log.d(TAG, "interfaceRemoved " + iface);
-        synchronized (mPublicSync) {
-            stopTrackingInterfaceLocked(iface);
-        }
+        startOrStopIpServer(iface, false /* enabled */);
     }
 
     void startTethering(final TetheringRequestParcel request, final IIntResultListener listener) {
@@ -578,12 +609,15 @@
 
     void stopTethering(int type) {
         mHandler.post(() -> {
-            mActiveTetheringRequests.remove(type);
-
-            enableTetheringInternal(type, false /* disabled */, null);
-            mEntitlementMgr.stopProvisioningIfNeeded(type);
+            stopTetheringInternal(type);
         });
     }
+    void stopTetheringInternal(int type) {
+        mActiveTetheringRequests.remove(type);
+
+        enableTetheringInternal(type, false /* disabled */, null);
+        mEntitlementMgr.stopProvisioningIfNeeded(type);
+    }
 
     /**
      * Enables or disables tethering for the given type. If provisioning is required, it will
@@ -635,17 +669,15 @@
     private int setWifiTethering(final boolean enable) {
         final long ident = Binder.clearCallingIdentity();
         try {
-            synchronized (mPublicSync) {
-                final WifiManager mgr = getWifiManager();
-                if (mgr == null) {
-                    mLog.e("setWifiTethering: failed to get WifiManager!");
-                    return TETHER_ERROR_SERVICE_UNAVAIL;
-                }
-                if ((enable && mgr.startTetheredHotspot(null /* use existing softap config */))
-                        || (!enable && mgr.stopSoftAp())) {
-                    mWifiTetherRequested = enable;
-                    return TETHER_ERROR_NO_ERROR;
-                }
+            final WifiManager mgr = getWifiManager();
+            if (mgr == null) {
+                mLog.e("setWifiTethering: failed to get WifiManager!");
+                return TETHER_ERROR_SERVICE_UNAVAIL;
+            }
+            if ((enable && mgr.startTetheredHotspot(null /* use existing softap config */))
+                    || (!enable && mgr.stopSoftAp())) {
+                mWifiTetherRequested = enable;
+                return TETHER_ERROR_NO_ERROR;
             }
         } finally {
             Binder.restoreCallingIdentity(ident);
@@ -697,25 +729,23 @@
     private int setEthernetTethering(final boolean enable) {
         final EthernetManager em = (EthernetManager) mContext.getSystemService(
                 Context.ETHERNET_SERVICE);
-        synchronized (mPublicSync) {
-            if (enable) {
-                if (mEthernetCallback != null) {
-                    Log.d(TAG, "Ethernet tethering already started");
-                    return TETHER_ERROR_NO_ERROR;
-                }
-
-                mEthernetCallback = new EthernetCallback();
-                mEthernetIfaceRequest = em.requestTetheredInterface(mExecutor, mEthernetCallback);
-            } else {
-                stopEthernetTetheringLocked();
+        if (enable) {
+            if (mEthernetCallback != null) {
+                Log.d(TAG, "Ethernet tethering already started");
+                return TETHER_ERROR_NO_ERROR;
             }
+
+            mEthernetCallback = new EthernetCallback();
+            mEthernetIfaceRequest = em.requestTetheredInterface(mExecutor, mEthernetCallback);
+        } else {
+            stopEthernetTethering();
         }
         return TETHER_ERROR_NO_ERROR;
     }
 
-    private void stopEthernetTetheringLocked() {
+    private void stopEthernetTethering() {
         if (mConfiguredEthernetIface != null) {
-            stopTrackingInterfaceLocked(mConfiguredEthernetIface);
+            ensureIpServerStopped(mConfiguredEthernetIface);
             mConfiguredEthernetIface = null;
         }
         if (mEthernetCallback != null) {
@@ -728,26 +758,21 @@
     private class EthernetCallback implements EthernetManager.TetheredInterfaceCallback {
         @Override
         public void onAvailable(String iface) {
-            synchronized (mPublicSync) {
-                if (this != mEthernetCallback) {
-                    // Ethernet callback arrived after Ethernet tethering stopped. Ignore.
-                    return;
-                }
-                maybeTrackNewInterfaceLocked(iface, TETHERING_ETHERNET);
-                changeInterfaceState(iface, getRequestedState(TETHERING_ETHERNET));
-                mConfiguredEthernetIface = iface;
+            if (this != mEthernetCallback) {
+                // Ethernet callback arrived after Ethernet tethering stopped. Ignore.
+                return;
             }
+            enableIpServing(TETHERING_ETHERNET, iface, getRequestedState(TETHERING_ETHERNET));
+            mConfiguredEthernetIface = iface;
         }
 
         @Override
         public void onUnavailable() {
-            synchronized (mPublicSync) {
-                if (this != mEthernetCallback) {
-                    // onAvailable called after stopping Ethernet tethering.
-                    return;
-                }
-                stopEthernetTetheringLocked();
+            if (this != mEthernetCallback) {
+                // onAvailable called after stopping Ethernet tethering.
+                return;
             }
+            stopEthernetTethering();
         }
     }
 
@@ -761,57 +786,53 @@
 
     private int tether(String iface, int requestedState) {
         if (DBG) Log.d(TAG, "Tethering " + iface);
-        synchronized (mPublicSync) {
-            TetherState tetherState = mTetherStates.get(iface);
-            if (tetherState == null) {
-                Log.e(TAG, "Tried to Tether an unknown iface: " + iface + ", ignoring");
-                return TETHER_ERROR_UNKNOWN_IFACE;
-            }
-            // Ignore the error status of the interface.  If the interface is available,
-            // the errors are referring to past tethering attempts anyway.
-            if (tetherState.lastState != IpServer.STATE_AVAILABLE) {
-                Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
-                return TETHER_ERROR_UNAVAIL_IFACE;
-            }
-            // NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's queue but not yet
-            // processed, this will be a no-op and it will not return an error.
-            //
-            // This code cannot race with untether() because they both synchronize on mPublicSync.
-            // TODO: reexamine the threading and messaging model to totally remove mPublicSync.
-            final int type = tetherState.ipServer.interfaceType();
-            final TetheringRequestParcel request = mActiveTetheringRequests.get(type, null);
-            if (request != null) {
-                mActiveTetheringRequests.delete(type);
-            }
-            tetherState.ipServer.sendMessage(IpServer.CMD_TETHER_REQUESTED, requestedState, 0,
-                    request);
-            return TETHER_ERROR_NO_ERROR;
+        TetherState tetherState = mTetherStates.get(iface);
+        if (tetherState == null) {
+            Log.e(TAG, "Tried to Tether an unknown iface: " + iface + ", ignoring");
+            return TETHER_ERROR_UNKNOWN_IFACE;
         }
+        // Ignore the error status of the interface.  If the interface is available,
+        // the errors are referring to past tethering attempts anyway.
+        if (tetherState.lastState != IpServer.STATE_AVAILABLE) {
+            Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
+            return TETHER_ERROR_UNAVAIL_IFACE;
+        }
+        // NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's queue but not yet
+        // processed, this will be a no-op and it will not return an error.
+        //
+        // This code cannot race with untether() because they both run on the handler thread.
+        final int type = tetherState.ipServer.interfaceType();
+        final TetheringRequestParcel request = mActiveTetheringRequests.get(type, null);
+        if (request != null) {
+            mActiveTetheringRequests.delete(type);
+        }
+        tetherState.ipServer.sendMessage(IpServer.CMD_TETHER_REQUESTED, requestedState, 0,
+                request);
+        return TETHER_ERROR_NO_ERROR;
     }
 
     void untether(String iface, final IIntResultListener listener) {
         mHandler.post(() -> {
             try {
                 listener.onResult(untether(iface));
-            } catch (RemoteException e) { }
+            } catch (RemoteException e) {
+            }
         });
     }
 
     int untether(String iface) {
         if (DBG) Log.d(TAG, "Untethering " + iface);
-        synchronized (mPublicSync) {
-            TetherState tetherState = mTetherStates.get(iface);
-            if (tetherState == null) {
-                Log.e(TAG, "Tried to Untether an unknown iface :" + iface + ", ignoring");
-                return TETHER_ERROR_UNKNOWN_IFACE;
-            }
-            if (!tetherState.isCurrentlyServing()) {
-                Log.e(TAG, "Tried to untether an inactive iface :" + iface + ", ignoring");
-                return TETHER_ERROR_UNAVAIL_IFACE;
-            }
-            tetherState.ipServer.sendMessage(IpServer.CMD_TETHER_UNREQUESTED);
-            return TETHER_ERROR_NO_ERROR;
+        TetherState tetherState = mTetherStates.get(iface);
+        if (tetherState == null) {
+            Log.e(TAG, "Tried to Untether an unknown iface :" + iface + ", ignoring");
+            return TETHER_ERROR_UNKNOWN_IFACE;
         }
+        if (!tetherState.isCurrentlyServing()) {
+            Log.e(TAG, "Tried to untether an inactive iface :" + iface + ", ignoring");
+            return TETHER_ERROR_UNAVAIL_IFACE;
+        }
+        tetherState.ipServer.sendMessage(IpServer.CMD_TETHER_UNREQUESTED);
+        return TETHER_ERROR_NO_ERROR;
     }
 
     void untetherAll() {
@@ -822,16 +843,15 @@
         stopTethering(TETHERING_ETHERNET);
     }
 
-    int getLastTetherError(String iface) {
-        synchronized (mPublicSync) {
-            TetherState tetherState = mTetherStates.get(iface);
-            if (tetherState == null) {
-                Log.e(TAG, "Tried to getLastTetherError on an unknown iface :" + iface
-                        + ", ignoring");
-                return TETHER_ERROR_UNKNOWN_IFACE;
-            }
-            return tetherState.lastError;
+    @VisibleForTesting
+    int getLastErrorForTest(String iface) {
+        TetherState tetherState = mTetherStates.get(iface);
+        if (tetherState == null) {
+            Log.e(TAG, "Tried to getLastErrorForTest on an unknown iface :" + iface
+                    + ", ignoring");
+            return TETHER_ERROR_UNKNOWN_IFACE;
         }
+        return tetherState.lastError;
     }
 
     private boolean isProvisioningNeededButUnavailable() {
@@ -874,74 +894,105 @@
                 : IpServer.STATE_TETHERED;
     }
 
+    private int getRequestedUsbType(boolean forNcmFunction) {
+        // TETHERING_NCM is only used if the device does not use NCM for regular USB tethering.
+        if (forNcmFunction && !mConfig.isUsingNcm()) return TETHERING_NCM;
+
+        return TETHERING_USB;
+    }
+
     // TODO: Figure out how to update for local hotspot mode interfaces.
     private void sendTetherStateChangedBroadcast() {
         if (!isTetheringSupported()) return;
 
-        final ArrayList<String> availableList = new ArrayList<>();
-        final ArrayList<String> tetherList = new ArrayList<>();
-        final ArrayList<String> localOnlyList = new ArrayList<>();
-        final ArrayList<String> erroredList = new ArrayList<>();
-        final ArrayList<Integer> lastErrorList = new ArrayList<>();
+        final ArrayList<TetheringInterface> available = new ArrayList<>();
+        final ArrayList<TetheringInterface> tethered = new ArrayList<>();
+        final ArrayList<TetheringInterface> localOnly = new ArrayList<>();
+        final ArrayList<TetheringInterface> errored = new ArrayList<>();
+        final ArrayList<Integer> lastErrors = new ArrayList<>();
 
         final TetheringConfiguration cfg = mConfig;
-        mTetherStatesParcel = new TetherStatesParcel();
 
         int downstreamTypesMask = DOWNSTREAM_NONE;
-        synchronized (mPublicSync) {
-            for (int i = 0; i < mTetherStates.size(); i++) {
-                TetherState tetherState = mTetherStates.valueAt(i);
-                String iface = mTetherStates.keyAt(i);
-                if (tetherState.lastError != TETHER_ERROR_NO_ERROR) {
-                    erroredList.add(iface);
-                    lastErrorList.add(tetherState.lastError);
-                } else if (tetherState.lastState == IpServer.STATE_AVAILABLE) {
-                    availableList.add(iface);
-                } else if (tetherState.lastState == IpServer.STATE_LOCAL_ONLY) {
-                    localOnlyList.add(iface);
-                } else if (tetherState.lastState == IpServer.STATE_TETHERED) {
-                    if (cfg.isUsb(iface)) {
-                        downstreamTypesMask |= (1 << TETHERING_USB);
-                    } else if (cfg.isWifi(iface)) {
-                        downstreamTypesMask |= (1 << TETHERING_WIFI);
-                    } else if (cfg.isBluetooth(iface)) {
-                        downstreamTypesMask |= (1 << TETHERING_BLUETOOTH);
-                    }
-                    tetherList.add(iface);
+        for (int i = 0; i < mTetherStates.size(); i++) {
+            final TetherState tetherState = mTetherStates.valueAt(i);
+            final int type = tetherState.ipServer.interfaceType();
+            final String iface = mTetherStates.keyAt(i);
+            final TetheringInterface tetheringIface = new TetheringInterface(type, iface);
+            if (tetherState.lastError != TETHER_ERROR_NO_ERROR) {
+                errored.add(tetheringIface);
+                lastErrors.add(tetherState.lastError);
+            } else if (tetherState.lastState == IpServer.STATE_AVAILABLE) {
+                available.add(tetheringIface);
+            } else if (tetherState.lastState == IpServer.STATE_LOCAL_ONLY) {
+                localOnly.add(tetheringIface);
+            } else if (tetherState.lastState == IpServer.STATE_TETHERED) {
+                switch (type) {
+                    case TETHERING_USB:
+                    case TETHERING_WIFI:
+                    case TETHERING_BLUETOOTH:
+                        downstreamTypesMask |= (1 << type);
+                        break;
+                    default:
+                        // Do nothing.
                 }
+                tethered.add(tetheringIface);
             }
         }
 
-        mTetherStatesParcel.availableList = availableList.toArray(new String[0]);
-        mTetherStatesParcel.tetheredList = tetherList.toArray(new String[0]);
-        mTetherStatesParcel.localOnlyList = localOnlyList.toArray(new String[0]);
-        mTetherStatesParcel.erroredIfaceList = erroredList.toArray(new String[0]);
-        mTetherStatesParcel.lastErrorList = new int[lastErrorList.size()];
-        Iterator<Integer> iterator = lastErrorList.iterator();
-        for (int i = 0; i < lastErrorList.size(); i++) {
-            mTetherStatesParcel.lastErrorList[i] = iterator.next().intValue();
-        }
+        mTetherStatesParcel = buildTetherStatesParcel(available, localOnly, tethered, errored,
+                lastErrors);
         reportTetherStateChanged(mTetherStatesParcel);
 
-        final Intent bcast = new Intent(ACTION_TETHER_STATE_CHANGED);
-        bcast.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
-        bcast.putStringArrayListExtra(EXTRA_AVAILABLE_TETHER, availableList);
-        bcast.putStringArrayListExtra(EXTRA_ACTIVE_LOCAL_ONLY, localOnlyList);
-        bcast.putStringArrayListExtra(EXTRA_ACTIVE_TETHER, tetherList);
-        bcast.putStringArrayListExtra(EXTRA_ERRORED_TETHER, erroredList);
-        mContext.sendStickyBroadcastAsUser(bcast, UserHandle.ALL);
+        mContext.sendStickyBroadcastAsUser(buildStateChangeIntent(available, localOnly, tethered,
+                errored), UserHandle.ALL);
         if (DBG) {
             Log.d(TAG, String.format(
-                    "sendTetherStateChangedBroadcast %s=[%s] %s=[%s] %s=[%s] %s=[%s]",
-                    "avail", TextUtils.join(",", availableList),
-                    "local_only", TextUtils.join(",", localOnlyList),
-                    "tether", TextUtils.join(",", tetherList),
-                    "error", TextUtils.join(",", erroredList)));
+                    "reportTetherStateChanged %s=[%s] %s=[%s] %s=[%s] %s=[%s]",
+                    "avail", TextUtils.join(",", available),
+                    "local_only", TextUtils.join(",", localOnly),
+                    "tether", TextUtils.join(",", tethered),
+                    "error", TextUtils.join(",", errored)));
         }
 
         mNotificationUpdater.onDownstreamChanged(downstreamTypesMask);
     }
 
+    private TetherStatesParcel buildTetherStatesParcel(
+            final ArrayList<TetheringInterface> available,
+            final ArrayList<TetheringInterface> localOnly,
+            final ArrayList<TetheringInterface> tethered,
+            final ArrayList<TetheringInterface> errored,
+            final ArrayList<Integer> lastErrors) {
+        final TetherStatesParcel parcel = new TetherStatesParcel();
+
+        parcel.availableList = available.toArray(new TetheringInterface[0]);
+        parcel.tetheredList = tethered.toArray(new TetheringInterface[0]);
+        parcel.localOnlyList = localOnly.toArray(new TetheringInterface[0]);
+        parcel.erroredIfaceList = errored.toArray(new TetheringInterface[0]);
+        parcel.lastErrorList = new int[lastErrors.size()];
+        for (int i = 0; i < lastErrors.size(); i++) {
+            parcel.lastErrorList[i] = lastErrors.get(i);
+        }
+
+        return parcel;
+    }
+
+    private Intent buildStateChangeIntent(final ArrayList<TetheringInterface> available,
+            final ArrayList<TetheringInterface> localOnly,
+            final ArrayList<TetheringInterface> tethered,
+            final ArrayList<TetheringInterface> errored) {
+        final Intent bcast = new Intent(ACTION_TETHER_STATE_CHANGED);
+        bcast.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+
+        bcast.putStringArrayListExtra(EXTRA_AVAILABLE_TETHER, toIfaces(available));
+        bcast.putStringArrayListExtra(EXTRA_ACTIVE_LOCAL_ONLY, toIfaces(localOnly));
+        bcast.putStringArrayListExtra(EXTRA_ACTIVE_TETHER, toIfaces(tethered));
+        bcast.putStringArrayListExtra(EXTRA_ERRORED_TETHER, toIfaces(errored));
+
+        return bcast;
+    }
+
     private class StateReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context content, Intent intent) {
@@ -988,8 +1039,8 @@
             final boolean rndisEnabled = intent.getBooleanExtra(USB_FUNCTION_RNDIS, false);
             final boolean ncmEnabled = intent.getBooleanExtra(USB_FUNCTION_NCM, false);
 
-            mLog.log(String.format("USB bcast connected:%s configured:%s rndis:%s",
-                    usbConnected, usbConfigured, rndisEnabled));
+            mLog.log(String.format("USB bcast connected:%s configured:%s rndis:%s ncm:%s",
+                    usbConnected, usbConfigured, rndisEnabled, ncmEnabled));
 
             // There are three types of ACTION_USB_STATE:
             //
@@ -1006,21 +1057,18 @@
             //       functions are ready to use.
             //
             // For more explanation, see b/62552150 .
-            synchronized (Tethering.this.mPublicSync) {
-                if (!usbConnected && mRndisEnabled) {
-                    // Turn off tethering if it was enabled and there is a disconnect.
-                    tetherMatchingInterfaces(IpServer.STATE_AVAILABLE, TETHERING_USB);
-                    mEntitlementMgr.stopProvisioningIfNeeded(TETHERING_USB);
-                } else if (usbConfigured && rndisEnabled) {
-                    // Tether if rndis is enabled and usb is configured.
-                    final int state = getRequestedState(TETHERING_USB);
-                    tetherMatchingInterfaces(state, TETHERING_USB);
-                } else if (usbConnected && ncmEnabled) {
-                    final int state = getRequestedState(TETHERING_NCM);
-                    tetherMatchingInterfaces(state, TETHERING_NCM);
-                }
-                mRndisEnabled = usbConfigured && rndisEnabled;
+            if (!usbConnected && (mRndisEnabled || mNcmEnabled)) {
+                // Turn off tethering if it was enabled and there is a disconnect.
+                disableUsbIpServing(TETHERING_USB);
+                mEntitlementMgr.stopProvisioningIfNeeded(TETHERING_USB);
+            } else if (usbConfigured && rndisEnabled) {
+                // Tether if rndis is enabled and usb is configured.
+                enableUsbIpServing(false /* isNcm */);
+            } else if (usbConfigured && ncmEnabled) {
+                enableUsbIpServing(true /* isNcm */);
             }
+            mRndisEnabled = usbConfigured && rndisEnabled;
+            mNcmEnabled = usbConfigured && ncmEnabled;
         }
 
         private void handleWifiApAction(Intent intent) {
@@ -1028,23 +1076,21 @@
             final String ifname = intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME);
             final int ipmode = intent.getIntExtra(EXTRA_WIFI_AP_MODE, IFACE_IP_MODE_UNSPECIFIED);
 
-            synchronized (Tethering.this.mPublicSync) {
-                switch (curState) {
-                    case WifiManager.WIFI_AP_STATE_ENABLING:
-                        // We can see this state on the way to both enabled and failure states.
-                        break;
-                    case WifiManager.WIFI_AP_STATE_ENABLED:
-                        enableWifiIpServingLocked(ifname, ipmode);
-                        break;
-                    case WifiManager.WIFI_AP_STATE_DISABLING:
-                        // We can see this state on the way to disabled.
-                        break;
-                    case WifiManager.WIFI_AP_STATE_DISABLED:
-                    case WifiManager.WIFI_AP_STATE_FAILED:
-                    default:
-                        disableWifiIpServingLocked(ifname, curState);
-                        break;
-                }
+            switch (curState) {
+                case WifiManager.WIFI_AP_STATE_ENABLING:
+                    // We can see this state on the way to both enabled and failure states.
+                    break;
+                case WifiManager.WIFI_AP_STATE_ENABLED:
+                    enableWifiIpServing(ifname, ipmode);
+                    break;
+                case WifiManager.WIFI_AP_STATE_DISABLING:
+                    // We can see this state on the way to disabled.
+                    break;
+                case WifiManager.WIFI_AP_STATE_DISABLED:
+                case WifiManager.WIFI_AP_STATE_FAILED:
+                default:
+                    disableWifiIpServing(ifname, curState);
+                    break;
             }
         }
 
@@ -1065,32 +1111,30 @@
                 Log.d(TAG, "WifiP2pAction: P2pInfo: " + p2pInfo + " Group: " + group);
             }
 
-            synchronized (Tethering.this.mPublicSync) {
-                // if no group is formed, bring it down if needed.
-                if (p2pInfo == null || !p2pInfo.groupFormed) {
-                    disableWifiP2pIpServingLockedIfNeeded(mWifiP2pTetherInterface);
-                    mWifiP2pTetherInterface = null;
-                    return;
-                }
-
-                // If there is a group but the device is not the owner, bail out.
-                if (!isGroupOwner(group)) return;
-
-                // If already serving from the correct interface, nothing to do.
-                if (group.getInterface().equals(mWifiP2pTetherInterface)) return;
-
-                // If already serving from another interface, turn it down first.
-                if (!TextUtils.isEmpty(mWifiP2pTetherInterface)) {
-                    mLog.w("P2P tethered interface " + mWifiP2pTetherInterface
-                            + "is different from current interface "
-                            + group.getInterface() + ", re-tether it");
-                    disableWifiP2pIpServingLockedIfNeeded(mWifiP2pTetherInterface);
-                }
-
-                // Finally bring up serving on the new interface
-                mWifiP2pTetherInterface = group.getInterface();
-                enableWifiIpServingLocked(mWifiP2pTetherInterface, IFACE_IP_MODE_LOCAL_ONLY);
+            // if no group is formed, bring it down if needed.
+            if (p2pInfo == null || !p2pInfo.groupFormed) {
+                disableWifiP2pIpServingIfNeeded(mWifiP2pTetherInterface);
+                mWifiP2pTetherInterface = null;
+                return;
             }
+
+            // If there is a group but the device is not the owner, bail out.
+            if (!isGroupOwner(group)) return;
+
+            // If already serving from the correct interface, nothing to do.
+            if (group.getInterface().equals(mWifiP2pTetherInterface)) return;
+
+            // If already serving from another interface, turn it down first.
+            if (!TextUtils.isEmpty(mWifiP2pTetherInterface)) {
+                mLog.w("P2P tethered interface " + mWifiP2pTetherInterface
+                        + "is different from current interface "
+                        + group.getInterface() + ", re-tether it");
+                disableWifiP2pIpServingIfNeeded(mWifiP2pTetherInterface);
+            }
+
+            // Finally bring up serving on the new interface
+            mWifiP2pTetherInterface = group.getInterface();
+            enableWifiIpServing(mWifiP2pTetherInterface, IFACE_IP_MODE_LOCAL_ONLY);
         }
 
         private void handleUserRestrictionAction() {
@@ -1125,14 +1169,14 @@
     @VisibleForTesting
     protected static class UserRestrictionActionListener {
         private final UserManager mUserMgr;
-        private final Tethering mWrapper;
+        private final Tethering mTethering;
         private final TetheringNotificationUpdater mNotificationUpdater;
         public boolean mDisallowTethering;
 
-        public UserRestrictionActionListener(@NonNull UserManager um, @NonNull Tethering wrapper,
+        public UserRestrictionActionListener(@NonNull UserManager um, @NonNull Tethering tethering,
                 @NonNull TetheringNotificationUpdater updater) {
             mUserMgr = um;
-            mWrapper = wrapper;
+            mTethering = tethering;
             mNotificationUpdater = updater;
             mDisallowTethering = false;
         }
@@ -1159,19 +1203,24 @@
                 return;
             }
 
-            if (mWrapper.isTetheringActive()) {
+            if (mTethering.isTetheringActive()) {
                 // Restricted notification is shown when tethering function is disallowed on
                 // user's device.
                 mNotificationUpdater.notifyTetheringDisabledByRestriction();
 
                 // Untether from all downstreams since tethering is disallowed.
-                mWrapper.untetherAll();
+                mTethering.untetherAll();
             }
             // TODO(b/148139325): send tetheringSupported on restriction change
         }
     }
 
-    private void disableWifiIpServingLockedCommon(int tetheringType, String ifname, int apState) {
+    private void enableIpServing(int tetheringType, String ifname, int ipServingMode) {
+        ensureIpServerStarted(ifname, tetheringType);
+        changeInterfaceState(ifname, ipServingMode);
+    }
+
+    private void disableWifiIpServingCommon(int tetheringType, String ifname, int apState) {
         mLog.log("Canceling WiFi tethering request -"
                 + " type=" + tetheringType
                 + " interface=" + ifname
@@ -1198,23 +1247,23 @@
                                            : "specified interface: " + ifname));
     }
 
-    private void disableWifiIpServingLocked(String ifname, int apState) {
+    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;
 
-        disableWifiIpServingLockedCommon(TETHERING_WIFI, ifname, apState);
+        disableWifiIpServingCommon(TETHERING_WIFI, ifname, apState);
     }
 
-    private void disableWifiP2pIpServingLockedIfNeeded(String ifname) {
+    private void disableWifiP2pIpServingIfNeeded(String ifname) {
         if (TextUtils.isEmpty(ifname)) return;
 
-        disableWifiIpServingLockedCommon(TETHERING_WIFI_P2P, ifname, /* fake */ 0);
+        disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname, /* fake */ 0);
     }
 
-    private void enableWifiIpServingLocked(String ifname, int wifiIpMode) {
+    private void enableWifiIpServing(String ifname, int wifiIpMode) {
         // Map wifiIpMode values to IpServer.Callback serving states, inferring
         // from mWifiTetherRequested as a final "best guess".
         final int ipServingMode;
@@ -1231,7 +1280,7 @@
         }
 
         if (!TextUtils.isEmpty(ifname)) {
-            maybeTrackNewInterfaceLocked(ifname);
+            ensureIpServerStarted(ifname);
             changeInterfaceState(ifname, ipServingMode);
         } else {
             mLog.e(String.format(
@@ -1246,18 +1295,17 @@
     //     - handles both enabling and disabling serving states
     //     - only tethers the first matching interface in listInterfaces()
     //       order of a given type
-    private void tetherMatchingInterfaces(int requestedState, int interfaceType) {
-        if (VDBG) {
-            Log.d(TAG, "tetherMatchingInterfaces(" + requestedState + ", " + interfaceType + ")");
-        }
-
+    private void enableUsbIpServing(boolean isNcm) {
+        final int interfaceType = getRequestedUsbType(isNcm);
+        final int requestedState = getRequestedState(interfaceType);
         String[] ifaces = null;
         try {
             ifaces = mNetd.interfaceGetList();
         } catch (RemoteException | ServiceSpecificException e) {
-            Log.e(TAG, "Error listing Interfaces", e);
+            mLog.e("Cannot enableUsbIpServing due to error listing Interfaces" + e);
             return;
         }
+
         String chosenIface = null;
         if (ifaces != null) {
             for (String iface : ifaces) {
@@ -1267,6 +1315,7 @@
                 }
             }
         }
+
         if (chosenIface == null) {
             Log.e(TAG, "could not find iface of type " + interfaceType);
             return;
@@ -1275,6 +1324,33 @@
         changeInterfaceState(chosenIface, requestedState);
     }
 
+    private void disableUsbIpServing(int interfaceType) {
+        String[] ifaces = null;
+        try {
+            ifaces = mNetd.interfaceGetList();
+        } catch (RemoteException | ServiceSpecificException e) {
+            mLog.e("Cannot disableUsbIpServing due to error listing Interfaces" + e);
+            return;
+        }
+
+        String chosenIface = null;
+        if (ifaces != null) {
+            for (String iface : ifaces) {
+                if (ifaceNameToType(iface) == interfaceType) {
+                    chosenIface = iface;
+                    break;
+                }
+            }
+        }
+
+        if (chosenIface == null) {
+            Log.e(TAG, "could not find iface of type " + interfaceType);
+            return;
+        }
+
+        changeInterfaceState(chosenIface, IpServer.STATE_AVAILABLE);
+    }
+
     private void changeInterfaceState(String ifname, int requestedState) {
         final int result;
         switch (requestedState) {
@@ -1312,21 +1388,15 @@
         return hasDownstreamConfiguration && hasUpstreamConfiguration;
     }
 
-    // TODO - update callers to use getTetheringConfiguration(),
-    // which has only final members.
-    String[] getTetherableUsbRegexs() {
-        return copy(mConfig.tetherableUsbRegexs);
+    void setUsbTethering(boolean enable, IIntResultListener listener) {
+        mHandler.post(() -> {
+            try {
+                listener.onResult(setUsbTethering(enable));
+            } catch (RemoteException e) { }
+        });
     }
 
-    String[] getTetherableWifiRegexs() {
-        return copy(mConfig.tetherableWifiRegexs);
-    }
-
-    String[] getTetherableBluetoothRegexs() {
-        return copy(mConfig.tetherableBluetoothRegexs);
-    }
-
-    int setUsbTethering(boolean enable) {
+    private int setUsbTethering(boolean enable) {
         if (VDBG) Log.d(TAG, "setUsbTethering(" + enable + ")");
         UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
         if (usbManager == null) {
@@ -1334,64 +1404,43 @@
             return TETHER_ERROR_SERVICE_UNAVAIL;
         }
 
-        synchronized (mPublicSync) {
-            usbManager.setCurrentFunctions(enable ? UsbManager.FUNCTION_RNDIS
-                    : UsbManager.FUNCTION_NONE);
-        }
+        final long usbFunction = mConfig.isUsingNcm()
+                ? UsbManager.FUNCTION_NCM : UsbManager.FUNCTION_RNDIS;
+        usbManager.setCurrentFunctions(enable ? usbFunction : UsbManager.FUNCTION_NONE);
+
         return TETHER_ERROR_NO_ERROR;
     }
 
     private int setNcmTethering(boolean enable) {
         if (VDBG) Log.d(TAG, "setNcmTethering(" + enable + ")");
+
+        // If TETHERING_USB is forced to use ncm function, TETHERING_NCM would no longer be
+        // available.
+        if (mConfig.isUsingNcm()) return TETHER_ERROR_SERVICE_UNAVAIL;
+
         UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
-        synchronized (mPublicSync) {
-            usbManager.setCurrentFunctions(enable ? UsbManager.FUNCTION_NCM
-                    : UsbManager.FUNCTION_NONE);
-        }
+        usbManager.setCurrentFunctions(enable ? UsbManager.FUNCTION_NCM : UsbManager.FUNCTION_NONE);
         return TETHER_ERROR_NO_ERROR;
     }
 
     // TODO review API - figure out how to delete these entirely.
     String[] getTetheredIfaces() {
         ArrayList<String> list = new ArrayList<String>();
-        synchronized (mPublicSync) {
-            for (int i = 0; i < mTetherStates.size(); i++) {
-                TetherState tetherState = mTetherStates.valueAt(i);
-                if (tetherState.lastState == IpServer.STATE_TETHERED) {
-                    list.add(mTetherStates.keyAt(i));
-                }
+        for (int i = 0; i < mTetherStates.size(); i++) {
+            TetherState tetherState = mTetherStates.valueAt(i);
+            if (tetherState.lastState == IpServer.STATE_TETHERED) {
+                list.add(mTetherStates.keyAt(i));
             }
         }
         return list.toArray(new String[list.size()]);
     }
 
-    String[] getTetherableIfaces() {
+    String[] getTetherableIfacesForTest() {
         ArrayList<String> list = new ArrayList<String>();
-        synchronized (mPublicSync) {
-            for (int i = 0; i < mTetherStates.size(); i++) {
-                TetherState tetherState = mTetherStates.valueAt(i);
-                if (tetherState.lastState == IpServer.STATE_AVAILABLE) {
-                    list.add(mTetherStates.keyAt(i));
-                }
-            }
-        }
-        return list.toArray(new String[list.size()]);
-    }
-
-    String[] getTetheredDhcpRanges() {
-        // TODO: this is only valid for the old DHCP server. Latest search suggests it is only used
-        // by WifiP2pServiceImpl to start dnsmasq: remove/deprecate after migrating callers.
-        return mConfig.legacyDhcpRanges;
-    }
-
-    String[] getErroredIfaces() {
-        ArrayList<String> list = new ArrayList<String>();
-        synchronized (mPublicSync) {
-            for (int i = 0; i < mTetherStates.size(); i++) {
-                TetherState tetherState = mTetherStates.valueAt(i);
-                if (tetherState.lastError != TETHER_ERROR_NO_ERROR) {
-                    list.add(mTetherStates.keyAt(i));
-                }
+        for (int i = 0; i < mTetherStates.size(); i++) {
+            TetherState tetherState = mTetherStates.valueAt(i);
+            if (tetherState.lastState == IpServer.STATE_AVAILABLE) {
+                list.add(mTetherStates.keyAt(i));
             }
         }
         return list.toArray(new String[list.size()]);
@@ -1403,10 +1452,7 @@
 
     private boolean upstreamWanted() {
         if (!mForwardedDownstreams.isEmpty()) return true;
-
-        synchronized (mPublicSync) {
-            return mWifiTetherRequested;
-        }
+        return mWifiTetherRequested;
     }
 
     // Needed because the canonical source of upstream truth is just the
@@ -2110,8 +2156,7 @@
     }
 
     private void startTrackDefaultNetwork() {
-        mUpstreamNetworkMonitor.startTrackDefaultNetwork(mDeps.getDefaultNetworkRequest(),
-                mEntitlementMgr);
+        mUpstreamNetworkMonitor.startTrackDefaultNetwork(mEntitlementMgr);
     }
 
     /** Get the latest value of the tethering entitlement check. */
@@ -2153,10 +2198,10 @@
 
     private TetherStatesParcel emptyTetherStatesParcel() {
         final TetherStatesParcel parcel = new TetherStatesParcel();
-        parcel.availableList = new String[0];
-        parcel.tetheredList = new String[0];
-        parcel.localOnlyList = new String[0];
-        parcel.erroredIfaceList = new String[0];
+        parcel.availableList = new TetheringInterface[0];
+        parcel.tetheredList = new TetheringInterface[0];
+        parcel.localOnlyList = new TetheringInterface[0];
+        parcel.erroredIfaceList = new TetheringInterface[0];
         parcel.lastErrorList = new int[0];
 
         return parcel;
@@ -2278,15 +2323,10 @@
         pw.decreaseIndent();
     }
 
-    void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) {
+    void doDump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) {
         // Binder.java closes the resource for us.
-        @SuppressWarnings("resource")
-        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
-        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
-                != PERMISSION_GRANTED) {
-            pw.println("Permission Denial: can't dump.");
-            return;
-        }
+        @SuppressWarnings("resource") final IndentingPrintWriter pw = new IndentingPrintWriter(
+                writer, "  ");
 
         if (argsContain(args, "bpf")) {
             dumpBpf(pw);
@@ -2307,37 +2347,35 @@
         mEntitlementMgr.dump(pw);
         pw.decreaseIndent();
 
-        synchronized (mPublicSync) {
-            pw.println("Tether state:");
-            pw.increaseIndent();
-            for (int i = 0; i < mTetherStates.size(); i++) {
-                final String iface = mTetherStates.keyAt(i);
-                final TetherState tetherState = mTetherStates.valueAt(i);
-                pw.print(iface + " - ");
+        pw.println("Tether state:");
+        pw.increaseIndent();
+        for (int i = 0; i < mTetherStates.size(); i++) {
+            final String iface = mTetherStates.keyAt(i);
+            final TetherState tetherState = mTetherStates.valueAt(i);
+            pw.print(iface + " - ");
 
-                switch (tetherState.lastState) {
-                    case IpServer.STATE_UNAVAILABLE:
-                        pw.print("UnavailableState");
-                        break;
-                    case IpServer.STATE_AVAILABLE:
-                        pw.print("AvailableState");
-                        break;
-                    case IpServer.STATE_TETHERED:
-                        pw.print("TetheredState");
-                        break;
-                    case IpServer.STATE_LOCAL_ONLY:
-                        pw.print("LocalHotspotState");
-                        break;
-                    default:
-                        pw.print("UnknownState");
-                        break;
-                }
-                pw.println(" - lastError = " + tetherState.lastError);
+            switch (tetherState.lastState) {
+                case IpServer.STATE_UNAVAILABLE:
+                    pw.print("UnavailableState");
+                    break;
+                case IpServer.STATE_AVAILABLE:
+                    pw.print("AvailableState");
+                    break;
+                case IpServer.STATE_TETHERED:
+                    pw.print("TetheredState");
+                    break;
+                case IpServer.STATE_LOCAL_ONLY:
+                    pw.print("LocalHotspotState");
+                    break;
+                default:
+                    pw.print("UnknownState");
+                    break;
             }
-            pw.println("Upstream wanted: " + upstreamWanted());
-            pw.println("Current upstream interface(s): " + mCurrentUpstreamIfaceSet);
-            pw.decreaseIndent();
+            pw.println(" - lastError = " + tetherState.lastError);
         }
+        pw.println("Upstream wanted: " + upstreamWanted());
+        pw.println("Current upstream interface(s): " + mCurrentUpstreamIfaceSet);
+        pw.decreaseIndent();
 
         pw.println("Hardware offload:");
         pw.increaseIndent();
@@ -2363,6 +2401,40 @@
         pw.decreaseIndent();
     }
 
+    void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) {
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PERMISSION_GRANTED) {
+            writer.println("Permission Denial: can't dump.");
+            return;
+        }
+
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        // Don't crash the system if something in doDump throws an exception, but try to propagate
+        // the exception to the caller.
+        AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>();
+        mHandler.post(() -> {
+            try {
+                doDump(fd, writer, args);
+            } catch (RuntimeException e) {
+                exceptionRef.set(e);
+            }
+            latch.countDown();
+        });
+
+        try {
+            if (!latch.await(DUMP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+                writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms");
+                return;
+            }
+        } catch (InterruptedException e) {
+            exceptionRef.compareAndSet(null, new IllegalStateException("Dump interrupted", e));
+        }
+
+        final RuntimeException e = exceptionRef.get();
+        if (e != null) throw e;
+    }
+
     private static boolean argsContain(String[] args, String target) {
         for (String arg : args) {
             if (target.equals(arg)) return true;
@@ -2404,14 +2476,12 @@
     // TODO: Move into TetherMainSM.
     private void notifyInterfaceStateChange(IpServer who, int state, int error) {
         final String iface = who.interfaceName();
-        synchronized (mPublicSync) {
-            final TetherState tetherState = mTetherStates.get(iface);
-            if (tetherState != null && tetherState.ipServer.equals(who)) {
-                tetherState.lastState = state;
-                tetherState.lastError = error;
-            } else {
-                if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
-            }
+        final TetherState tetherState = mTetherStates.get(iface);
+        if (tetherState != null && tetherState.ipServer.equals(who)) {
+            tetherState.lastState = state;
+            tetherState.lastError = error;
+        } else {
+            if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
         }
 
         mLog.log(String.format("OBSERVED iface=%s state=%s error=%s", iface, state, error));
@@ -2443,14 +2513,12 @@
     private void notifyLinkPropertiesChanged(IpServer who, LinkProperties newLp) {
         final String iface = who.interfaceName();
         final int state;
-        synchronized (mPublicSync) {
-            final TetherState tetherState = mTetherStates.get(iface);
-            if (tetherState != null && tetherState.ipServer.equals(who)) {
-                state = tetherState.lastState;
-            } else {
-                mLog.log("got notification from stale iface " + iface);
-                return;
-            }
+        final TetherState tetherState = mTetherStates.get(iface);
+        if (tetherState != null && tetherState.ipServer.equals(who)) {
+            state = tetherState.lastState;
+        } else {
+            mLog.log("got notification from stale iface " + iface);
+            return;
         }
 
         mLog.log(String.format(
@@ -2460,7 +2528,7 @@
         mTetherMainSM.sendMessage(which, state, 0, newLp);
     }
 
-    private void maybeTrackNewInterfaceLocked(final String iface) {
+    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) {
@@ -2480,17 +2548,17 @@
             return;
         }
 
-        maybeTrackNewInterfaceLocked(iface, interfaceType);
+        ensureIpServerStarted(iface, interfaceType);
     }
 
-    private void maybeTrackNewInterfaceLocked(final String iface, int interfaceType) {
+    private void ensureIpServerStarted(final String iface, int interfaceType) {
         // If we have already started a TISM for this interface, skip.
         if (mTetherStates.containsKey(iface)) {
             mLog.log("active iface (" + iface + ") reported as added, ignoring");
             return;
         }
 
-        mLog.log("adding TetheringInterfaceStateMachine for: " + iface);
+        mLog.log("adding IpServer for: " + iface);
         final TetherState tetherState = new TetherState(
                 new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
                              makeControlCallback(), mConfig.enableLegacyDhcpServer,
@@ -2500,14 +2568,12 @@
         tetherState.ipServer.start();
     }
 
-    private void stopTrackingInterfaceLocked(final String iface) {
+    private void ensureIpServerStopped(final String iface) {
         final TetherState tetherState = mTetherStates.get(iface);
-        if (tetherState == null) {
-            mLog.log("attempting to remove unknown iface (" + iface + "), ignoring");
-            return;
-        }
+        if (tetherState == null) return;
+
         tetherState.ipServer.stop();
-        mLog.log("removing TetheringInterfaceStateMachine for: " + iface);
+        mLog.log("removing IpServer for: " + iface);
         mTetherStates.remove(iface);
     }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 413b0cb..31fcea4 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -23,11 +23,13 @@
 import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
 import android.net.TetheringConfigurationParcel;
 import android.net.util.SharedLog;
 import android.provider.DeviceConfig;
+import android.provider.Settings;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
@@ -58,6 +60,8 @@
 
     private static final String[] EMPTY_STRING_ARRAY = new String[0];
 
+    private static final String TETHERING_MODULE_NAME = "com.android.tethering";
+
     // Default ranges used for the legacy DHCP server.
     // USB is  192.168.42.1 and 255.255.255.0
     // Wifi is 192.168.43.1 and 255.255.255.0
@@ -73,6 +77,12 @@
 
     private static final String[] DEFAULT_IPV4_DNS = {"8.8.4.4", "8.8.8.8"};
 
+    @VisibleForTesting
+    public static final int TETHER_USB_RNDIS_FUNCTION = 0;
+
+    @VisibleForTesting
+    public static final int TETHER_USB_NCM_FUNCTION   = 1;
+
     /**
      * Override enabling BPF offload configuration for tethering.
      */
@@ -102,7 +112,7 @@
      * via resource overlays, and later noticed issues. To that end, it overrides
      * config_tether_upstream_automatic when set to true.
      *
-     * This flag is enabled if !=0 and less than the module APK version: see
+     * This flag is enabled if !=0 and less than the module APEX version: see
      * {@link DeviceConfigUtils#isFeatureEnabled}. It is also ignored after R, as later devices
      * should just set config_tether_upstream_automatic to true instead.
      */
@@ -110,6 +120,13 @@
             "tether_force_upstream_automatic_version";
 
     /**
+     * Settings key to foce choosing usb functions for usb tethering.
+     *
+     * TODO: Remove this hard code string and make Settings#TETHER_FORCE_USB_FUNCTIONS as API.
+     */
+    public static final String TETHER_FORCE_USB_FUNCTIONS =
+            "tether_force_usb_functions";
+    /**
      * Default value that used to periodic polls tether offload stats from tethering offload HAL
      * to make the data warnings work.
      */
@@ -141,12 +158,17 @@
     private final boolean mEnableWifiP2pDedicatedIp;
 
     private final boolean mEnableSelectAllPrefixRange;
+    private final int mUsbTetheringFunction;
+    protected final ContentResolver mContentResolver;
 
     public TetheringConfiguration(Context ctx, SharedLog log, int id) {
         final SharedLog configLog = log.forSubComponent("config");
 
         activeDataSubId = id;
         Resources res = getResources(ctx, activeDataSubId);
+        mContentResolver = ctx.getContentResolver();
+
+        mUsbTetheringFunction = getUsbTetheringFunction(res);
 
         tetherableUsbRegexs = getResourceStringArray(res, R.array.config_tether_usb_regexs);
         tetherableNcmRegexs = getResourceStringArray(res, R.array.config_tether_ncm_regexs);
@@ -198,6 +220,11 @@
         configLog.log(toString());
     }
 
+    /** Check whether using ncm for usb tethering */
+    public boolean isUsingNcm() {
+        return mUsbTetheringFunction == TETHER_USB_NCM_FUNCTION;
+    }
+
     /** Check whether input interface belong to usb.*/
     public boolean isUsb(String iface) {
         return matchesDownstreamRegexs(iface, tetherableUsbRegexs);
@@ -283,6 +310,9 @@
 
         pw.print("mEnableSelectAllPrefixRange: ");
         pw.println(mEnableSelectAllPrefixRange);
+
+        pw.print("mUsbTetheringFunction: ");
+        pw.println(isUsingNcm() ? "NCM" : "RNDIS");
     }
 
     /** Returns the string representation of this object.*/
@@ -348,6 +378,26 @@
         return mEnableSelectAllPrefixRange;
     }
 
+    private int getUsbTetheringFunction(Resources res) {
+        final int valueFromRes = getResourceInteger(res, R.integer.config_tether_usb_functions,
+                TETHER_USB_RNDIS_FUNCTION /* defaultValue */);
+        return getSettingsIntValue(TETHER_FORCE_USB_FUNCTIONS, valueFromRes);
+    }
+
+    private int getSettingsIntValue(final String name, final int defaultValue) {
+        final String value = getSettingsValue(name);
+        try {
+            return value != null ? Integer.parseInt(value) : defaultValue;
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+
+    @VisibleForTesting
+    protected String getSettingsValue(final String name) {
+        return Settings.Global.getString(mContentResolver, name);
+    }
+
     private static Collection<Integer> getUpstreamIfaceTypes(Resources res, boolean dunRequired) {
         final int[] ifaceTypes = res.getIntArray(R.array.config_tether_upstream_types);
         final ArrayList<Integer> upstreamIfaceTypes = new ArrayList<>(ifaceTypes.length);
@@ -473,7 +523,8 @@
 
     @VisibleForTesting
     protected boolean isFeatureEnabled(Context ctx, String featureVersionFlag) {
-        return DeviceConfigUtils.isFeatureEnabled(ctx, NAMESPACE_CONNECTIVITY, featureVersionFlag);
+        return DeviceConfigUtils.isFeatureEnabled(ctx, NAMESPACE_CONNECTIVITY, featureVersionFlag,
+                TETHERING_MODULE_NAME, false /* defaultEnabled */);
     }
 
     private Resources getResources(Context ctx, int subId) {
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 45b9141..7df9475 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -20,7 +20,6 @@
 import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.net.INetd;
-import android.net.NetworkRequest;
 import android.net.ip.IpServer;
 import android.net.util.SharedLog;
 import android.os.Handler;
@@ -99,11 +98,6 @@
     }
 
     /**
-     * Get the NetworkRequest that should be fulfilled by the default network.
-     */
-    public abstract NetworkRequest getDefaultNetworkRequest();
-
-    /**
      * Get a reference to the EntitlementManager to be used by tethering.
      */
     public EntitlementManager getEntitlementManager(Context ctx, Handler h, SharedLog log,
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index e36df7f..5ab3401 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -35,8 +35,6 @@
 import android.net.INetworkStackConnector;
 import android.net.ITetheringConnector;
 import android.net.ITetheringEventCallback;
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
 import android.net.NetworkStack;
 import android.net.TetheringRequestParcel;
 import android.net.dhcp.DhcpServerCallbacks;
@@ -121,9 +119,7 @@
                 IIntResultListener listener) {
             if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
 
-            try {
-                listener.onResult(mTethering.setUsbTethering(enable));
-            } catch (RemoteException e) { }
+            mTethering.setUsbTethering(enable, listener);
         }
 
         @Override
@@ -309,19 +305,6 @@
     public TetheringDependencies makeTetheringDependencies() {
         return new TetheringDependencies() {
             @Override
-            public NetworkRequest getDefaultNetworkRequest() {
-                // TODO: b/147280869, add a proper system API to replace this.
-                final NetworkRequest trackDefaultRequest = new NetworkRequest.Builder()
-                        .clearCapabilities()
-                        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
-                        .addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
-                        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
-                        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
-                        .build();
-                return trackDefaultRequest;
-            }
-
-            @Override
             public Looper getTetheringLooper() {
                 final HandlerThread tetherThread = new HandlerThread("android.tethering");
                 tetherThread.start();
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index 9de4c87..69471a1 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -47,6 +47,9 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.StateMachine;
+import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
+import com.android.networkstack.apishim.common.ConnectivityManagerShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 
 import java.util.HashMap;
 import java.util.HashSet;
@@ -142,33 +145,28 @@
         mWhat = what;
         mLocalPrefixes = new HashSet<>();
         mIsDefaultCellularUpstream = false;
-    }
-
-    @VisibleForTesting
-    public UpstreamNetworkMonitor(
-            ConnectivityManager cm, StateMachine tgt, SharedLog log, int what) {
-        this((Context) null, tgt, log, what);
-        mCM = cm;
+        mCM = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
     }
 
     /**
-     * Tracking the system default network. This method should be called when system is ready.
+     * Tracking the system default network. This method should be only called once when system is
+     * ready, and the callback is never unregistered.
      *
-     * @param defaultNetworkRequest should be the same as ConnectivityService default request
      * @param entitle a EntitlementManager object to communicate between EntitlementManager and
      * UpstreamNetworkMonitor
      */
-    public void startTrackDefaultNetwork(NetworkRequest defaultNetworkRequest,
-            EntitlementManager entitle) {
-
-        // defaultNetworkRequest is not really a "request", just a way of tracking the system
-        // default network. It's guaranteed not to actually bring up any networks because it's
-        // the should be the same request as the ConnectivityService default request, and thus
-        // shares fate with it. We can't use registerDefaultNetworkCallback because it will not
-        // track the system default network if there is a VPN that applies to our UID.
-        if (mDefaultNetworkCallback == null) {
-            mDefaultNetworkCallback = new UpstreamNetworkCallback(CALLBACK_DEFAULT_INTERNET);
-            cm().requestNetwork(defaultNetworkRequest, mDefaultNetworkCallback, mHandler);
+    public void startTrackDefaultNetwork(EntitlementManager entitle) {
+        if (mDefaultNetworkCallback != null) {
+            Log.wtf(TAG, "default network callback is already registered");
+            return;
+        }
+        ConnectivityManagerShim mCmShim = ConnectivityManagerShimImpl.newInstance(mContext);
+        mDefaultNetworkCallback = new UpstreamNetworkCallback(CALLBACK_DEFAULT_INTERNET);
+        try {
+            mCmShim.registerSystemDefaultNetworkCallback(mDefaultNetworkCallback, mHandler);
+        } catch (UnsupportedApiLevelException e) {
+            Log.wtf(TAG, "registerSystemDefaultNetworkCallback is not supported");
+            return;
         }
         if (mEntitlementMgr == null) {
             mEntitlementMgr = entitle;
@@ -193,7 +191,7 @@
      * check even tethering is not active yet.
      */
     public void stop() {
-        releaseMobileNetworkRequest();
+        setTryCell(false);
 
         releaseCallback(mListenAllCallback);
         mListenAllCallback = null;
@@ -400,10 +398,10 @@
             // notifications (e.g. matching more than one of our callbacks).
             //
             // Also, it can happen that onLinkPropertiesChanged is called after
-            // onLost removed the state from mNetworkMap. This appears to be due
-            // to a bug in disconnectAndDestroyNetwork, which calls
-            // nai.clatd.update() after the onLost callbacks.
-            // TODO: fix the bug and make this method void.
+            // onLost removed the state from mNetworkMap. This is due to a bug
+            // in disconnectAndDestroyNetwork, which calls nai.clatd.update()
+            // after the onLost callbacks. This was fixed in S.
+            // TODO: make this method void when R is no longer supported.
             return null;
         }
 
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 6999ed9..e807613 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -23,6 +23,7 @@
         "src/**/*.java",
         "src/**/*.kt",
     ],
+    min_sdk_version: "30",
     static_libs: [
         "NetworkStackApiStableLib",
         "androidx.test.rules",
@@ -44,12 +45,25 @@
 }
 
 android_library {
-    name: "TetheringIntegrationTestsLib",
+    name: "TetheringIntegrationTestsLatestSdkLib",
+    target_sdk_version: "30",
     platform_apis: true,
     defaults: ["TetheringIntegrationTestsDefaults"],
     visibility: [
-        "//cts/tests/tests/tethering",
         "//packages/modules/Connectivity/tests/cts/tethering",
+        "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
+    ]
+}
+
+android_library {
+    name: "TetheringIntegrationTestsLib",
+    target_sdk_version: "current",
+    platform_apis: true,
+    defaults: ["TetheringIntegrationTestsDefaults"],
+    visibility: [
+        "//packages/modules/Connectivity/tests/cts/tethering",
+        "//packages/modules/Connectivity/Tethering/tests/mts",
     ]
 }
 
@@ -70,6 +84,8 @@
 android_test {
     name: "TetheringCoverageTests",
     platform_apis: true,
+    min_sdk_version: "30",
+    target_sdk_version: "30",
     test_suites: ["device-tests", "mts"],
     test_config: "AndroidTest_Coverage.xml",
     defaults: ["libnetworkstackutilsjni_deps"],
@@ -78,8 +94,8 @@
         "NetdStaticLibTestsLib",
         "NetworkStaticLibTestsLib",
         "NetworkStackTestsLib",
-        "TetheringTestsLib",
-        "TetheringIntegrationTestsLib",
+        "TetheringTestsLatestSdkLib",
+        "TetheringIntegrationTestsLatestSdkLib",
     ],
     jni_libs: [
         // For mockito extended
@@ -89,7 +105,6 @@
         "libnetworkstackutilsjni",
         "libtetherutilsjni",
     ],
-    jarjar_rules: ":TetheringTestsJarJarRules",
     compile_multilib: "both",
     manifest: "AndroidManifest_coverage.xml",
 }
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index fe4e696..f1ddc6d 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -16,6 +16,7 @@
 
 package android.net;
 
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
 import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.Manifest.permission.TETHER_PRIVILEGED;
@@ -79,6 +80,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -121,7 +123,7 @@
         // Needed to create a TestNetworkInterface, to call requestTetheredInterface, and to receive
         // tethered client callbacks.
         mUiAutomation.adoptShellPermissionIdentity(
-                MANAGE_TEST_NETWORKS, NETWORK_SETTINGS, TETHER_PRIVILEGED);
+                MANAGE_TEST_NETWORKS, NETWORK_SETTINGS, TETHER_PRIVILEGED, ACCESS_NETWORK_STATE);
         mRunTests = mTm.isTetheringSupported() && mEm != null;
         assumeTrue(mRunTests);
 
@@ -348,7 +350,7 @@
         private final CountDownLatch mLocalOnlyStartedLatch = new CountDownLatch(1);
         private final CountDownLatch mLocalOnlyStoppedLatch = new CountDownLatch(1);
         private final CountDownLatch mClientConnectedLatch = new CountDownLatch(1);
-        private final String mIface;
+        private final TetheringInterface mIface;
 
         private volatile boolean mInterfaceWasTethered = false;
         private volatile boolean mInterfaceWasLocalOnly = false;
@@ -357,20 +359,24 @@
 
         MyTetheringEventCallback(TetheringManager tm, String iface) {
             mTm = tm;
-            mIface = iface;
+            mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
         }
 
         public void unregister() {
             mTm.unregisterTetheringEventCallback(this);
             mUnregistered = true;
         }
-
         @Override
         public void onTetheredInterfacesChanged(List<String> interfaces) {
+            fail("Should only call callback that takes a Set<TetheringInterface>");
+        }
+
+        @Override
+        public void onTetheredInterfacesChanged(Set<TetheringInterface> interfaces) {
             // Ignore stale callbacks registered by previous test cases.
             if (mUnregistered) return;
 
-            if (!mInterfaceWasTethered && (mIface == null || interfaces.contains(mIface))) {
+            if (!mInterfaceWasTethered && interfaces.contains(mIface)) {
                 // This interface is being tethered for the first time.
                 Log.d(TAG, "Tethering started: " + interfaces);
                 mInterfaceWasTethered = true;
@@ -383,10 +389,15 @@
 
         @Override
         public void onLocalOnlyInterfacesChanged(List<String> interfaces) {
+            fail("Should only call callback that takes a Set<TetheringInterface>");
+        }
+
+        @Override
+        public void onLocalOnlyInterfacesChanged(Set<TetheringInterface> interfaces) {
             // Ignore stale callbacks registered by previous test cases.
             if (mUnregistered) return;
 
-            if (!mInterfaceWasLocalOnly && (mIface == null || interfaces.contains(mIface))) {
+            if (!mInterfaceWasLocalOnly && interfaces.contains(mIface)) {
                 // This interface is being put into local-only mode for the first time.
                 Log.d(TAG, "Local-only started: " + interfaces);
                 mInterfaceWasLocalOnly = true;
diff --git a/Tethering/tests/jarjar-rules.txt b/Tethering/tests/jarjar-rules.txt
index c99ff7f..9cb143e 100644
--- a/Tethering/tests/jarjar-rules.txt
+++ b/Tethering/tests/jarjar-rules.txt
@@ -1,8 +1,8 @@
 # Don't jar-jar the entire package because this test use some
 # internal classes (like ArrayUtils in com.android.internal.util)
 rule com.android.internal.util.BitUtils* com.android.networkstack.tethering.util.BitUtils@1
-rule com.android.internal.util.IndentingPrintWriter.java* com.android.networkstack.tethering.util.IndentingPrintWriter.java@1
-rule com.android.internal.util.IState.java* com.android.networkstack.tethering.util.IState.java@1
+rule com.android.internal.util.IndentingPrintWriter* com.android.networkstack.tethering.util.IndentingPrintWriter@1
+rule com.android.internal.util.IState* com.android.networkstack.tethering.util.IState@1
 rule com.android.internal.util.MessageUtils* com.android.networkstack.tethering.util.MessageUtils@1
 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
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index edb6356..221771f 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -19,7 +19,10 @@
 android_test {
     // This tests for functionality that is not required for devices that
     // don't use Tethering mainline module.
-    name: "MtsTetheringTest",
+    name: "MtsTetheringTestLatestSdk",
+
+    min_sdk_version: "30",
+    target_sdk_version: "30",
 
     libs: [
         "android.test.base",
diff --git a/Tethering/tests/mts/AndroidTest.xml b/Tethering/tests/mts/AndroidTest.xml
index 80788df..4edd544 100644
--- a/Tethering/tests/mts/AndroidTest.xml
+++ b/Tethering/tests/mts/AndroidTest.xml
@@ -24,7 +24,7 @@
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
-        <option name="test-file-name" value="MtsTetheringTest.apk" />
+        <option name="test-file-name" value="MtsTetheringTestLatestSdk.apk" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.tethering.mts" />
diff --git a/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java b/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java
index 7ffe37a..07aab63 100644
--- a/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java
+++ b/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java
@@ -15,17 +15,18 @@
  */
 package android.tethering.mts;
 
+import static android.Manifest.permission.ACCESS_WIFI_STATE;
 import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.Manifest.permission.TETHER_PRIVILEGED;
 import static android.Manifest.permission.WRITE_SETTINGS;
+import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.cts.util.CtsTetheringUtils.isWifiTetheringSupported;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 
 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.fail;
@@ -35,6 +36,7 @@
 import android.content.Context;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
+import android.net.TetheringInterface;
 import android.net.TetheringManager;
 import android.net.cts.util.CtsTetheringUtils;
 import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
@@ -69,7 +71,7 @@
     @Before
     public void setUp() throws Exception {
         mUiAutomation.adoptShellPermissionIdentity(MANAGE_TEST_NETWORKS, NETWORK_SETTINGS,
-                WRITE_SETTINGS, READ_DEVICE_CONFIG, TETHER_PRIVILEGED);
+                WRITE_SETTINGS, READ_DEVICE_CONFIG, TETHER_PRIVILEGED, ACCESS_WIFI_STATE);
         mContext = InstrumentationRegistry.getContext();
         mTm = mContext.getSystemService(TetheringManager.class);
         mCtsTetheringUtils = new CtsTetheringUtils(mContext);
@@ -101,13 +103,14 @@
         TestNetworkTracker tnt = null;
         try {
             tetherEventCallback.assumeTetheringSupported();
-            assumeTrue(isWifiTetheringSupported(tetherEventCallback));
+            assumeTrue(isWifiTetheringSupported(mContext, tetherEventCallback));
+            tetherEventCallback.expectNoTetheringActive();
 
-            mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+            final TetheringInterface tetheredIface =
+                    mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
 
-            final List<String> tetheredIfaces = tetherEventCallback.getTetheredInterfaces();
-            assertEquals(1, tetheredIfaces.size());
-            final String wifiTetheringIface = tetheredIfaces.get(0);
+            assertNotNull(tetheredIface);
+            final String wifiTetheringIface = tetheredIface.getInterface();
 
             NetworkInterface nif = NetworkInterface.getByName(wifiTetheringIface);
             // Tethering downstream only have one ipv4 address.
@@ -120,11 +123,11 @@
             tnt = setUpTestNetwork(
                     new LinkAddress(testPrefix.getAddress(), testPrefix.getPrefixLength()));
 
-            tetherEventCallback.expectTetheredInterfacesChanged(null);
+            tetherEventCallback.expectNoTetheringActive();
             final List<String> wifiRegexs =
                     tetherEventCallback.getTetheringInterfaceRegexps().getTetherableWifiRegexs();
 
-            tetherEventCallback.expectTetheredInterfacesChanged(wifiRegexs);
+            tetherEventCallback.expectTetheredInterfacesChanged(wifiRegexs, TETHERING_WIFI);
             nif = NetworkInterface.getByName(wifiTetheringIface);
             final LinkAddress newHotspotAddr = getFirstIpv4Address(nif);
             assertNotNull(newHotspotAddr);
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index d469b37..192a540 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -36,13 +36,13 @@
         "framework-tethering.impl",
     ],
     visibility: [
-        "//cts/tests/tests/tethering",
         "//packages/modules/Connectivity/tests/cts/tethering",
     ],
 }
 
 java_defaults {
     name: "TetheringTestsDefaults",
+    min_sdk_version: "30",
     srcs: [
         "src/**/*.java",
         "src/**/*.kt",
@@ -75,17 +75,19 @@
         "libstaticjvmtiagent",
         "libtetherutilsjni",
     ],
+    jarjar_rules: ":TetheringTestsJarJarRules",
 }
 
 // Library containing the unit tests. This is used by the coverage test target to pull in the
 // unit test code. It is not currently used by the tests themselves because all the build
 // configuration needed by the tests is in the TetheringTestsDefaults rule.
 android_library {
-    name: "TetheringTestsLib",
+    name: "TetheringTestsLatestSdkLib",
     defaults: ["TetheringTestsDefaults"],
+    target_sdk_version: "30",
     visibility: [
-        "//frameworks/base/packages/Tethering/tests/integration",
-        "//packages/modules/Connectivity/Tethering/tests/integration",
+        "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
     ]
 }
 
@@ -96,7 +98,6 @@
         "device-tests",
         "mts",
     ],
-    jarjar_rules: ":TetheringTestsJarJarRules",
     defaults: ["TetheringTestsDefaults"],
     compile_multilib: "both",
 }
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 435cab5..ce69cb3 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -107,6 +107,8 @@
 import com.android.networkstack.tethering.Tether4Key;
 import com.android.networkstack.tethering.Tether4Value;
 import com.android.networkstack.tethering.Tether6Value;
+import com.android.networkstack.tethering.TetherDevKey;
+import com.android.networkstack.tethering.TetherDevValue;
 import com.android.networkstack.tethering.TetherDownstream6Key;
 import com.android.networkstack.tethering.TetherLimitKey;
 import com.android.networkstack.tethering.TetherLimitValue;
@@ -182,6 +184,7 @@
     @Mock private BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
     @Mock private BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
     @Mock private BpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
+    @Mock private BpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
 
     @Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
 
@@ -334,6 +337,11 @@
                     public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
                         return mBpfLimitMap;
                     }
+
+                    @Nullable
+                    public BpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() {
+                        return mBpfDevMap;
+                    }
                 };
         mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
 
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 233f6db..cc912f4 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -207,6 +207,7 @@
     @Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
     @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
     @Mock private BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
+    @Mock private BpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
 
     // Late init since methods must be called by the thread that created this object.
     private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
@@ -284,6 +285,11 @@
                     public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
                         return mBpfLimitMap;
                     }
+
+                    @Nullable
+                    public BpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() {
+                        return mBpfDevMap;
+                    }
             });
 
     @Before public void setUp() {
@@ -1368,12 +1374,9 @@
         coordinator.tetherOffloadClientAdd(mIpServer, clientInfo);
     }
 
-    // TODO: Test the IPv4 and IPv6 exist concurrently.
-    // TODO: Test the IPv4 rule delete failed.
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.R)
-    public void testSetDataLimitOnRule4Change() throws Exception {
-        final BpfCoordinator coordinator = makeBpfCoordinator();
+    private void initBpfCoordinatorForRule4(final BpfCoordinator coordinator) throws Exception {
+        // Needed because addUpstreamIfindexToMap only updates upstream information when polling
+        // was started.
         coordinator.startPolling();
 
         // Needed because tetherOffloadRuleRemove of api31.BpfCoordinatorShimImpl only decreases
@@ -1387,6 +1390,15 @@
         coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
         setUpstreamInformationTo(coordinator);
         setDownstreamAndClientInformationTo(coordinator);
+    }
+
+    // TODO: Test the IPv4 and IPv6 exist concurrently.
+    // TODO: Test the IPv4 rule delete failed.
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testSetDataLimitOnRule4Change() throws Exception {
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+        initBpfCoordinatorForRule4(coordinator);
 
         // Applying a data limit to the current upstream does not take any immediate action.
         // The data limit could be only set on an upstream which has rules.
@@ -1445,4 +1457,41 @@
         verifyTetherOffloadGetAndClearStats(inOrder, UPSTREAM_IFINDEX);
         inOrder.verifyNoMoreInteractions();
     }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testAddDevMapRule6() throws Exception {
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+
+        coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
+        final Ipv6ForwardingRule ruleA = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
+        final Ipv6ForwardingRule ruleB = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_B, MAC_B);
+
+        coordinator.tetherOffloadRuleAdd(mIpServer, ruleA);
+        verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
+                eq(new TetherDevValue(UPSTREAM_IFINDEX)));
+        verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)),
+                eq(new TetherDevValue(DOWNSTREAM_IFINDEX)));
+        clearInvocations(mBpfDevMap);
+
+        coordinator.tetherOffloadRuleAdd(mIpServer, ruleB);
+        verify(mBpfDevMap, never()).updateEntry(any(), any());
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testAddDevMapRule4() throws Exception {
+        final BpfCoordinator coordinator = makeBpfCoordinator();
+        initBpfCoordinatorForRule4(coordinator);
+
+        mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_TCP));
+        verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
+                eq(new TetherDevValue(UPSTREAM_IFINDEX)));
+        verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)),
+                eq(new TetherDevValue(DOWNSTREAM_IFINDEX)));
+        clearInvocations(mBpfDevMap);
+
+        mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP));
+        verify(mBpfDevMap, never()).updateEntry(any(), any());
+    }
 }
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 8cfa7d0..442be1e 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -53,6 +53,7 @@
 
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ModuleInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
@@ -203,6 +204,7 @@
         doReturn(mPm).when(mContext).getPackageManager();
         doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName();
         doReturn(new PackageInfo()).when(mPm).getPackageInfo(anyString(), anyInt());
+        doReturn(new ModuleInfo()).when(mPm).getModuleInfo(anyString(), anyInt());
 
         when(mResources.getStringArray(R.array.config_tether_dhcp_range))
                 .thenReturn(new String[0]);
@@ -224,7 +226,7 @@
         mEnMgr = new WrappedEntitlementManager(mMockContext, new Handler(mLooper.getLooper()), mLog,
                 mPermissionChangeCallback);
         mEnMgr.setOnUiEntitlementFailedListener(mEntitlementFailedListener);
-        mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
         mEnMgr.setTetheringConfigurationFetcher(() -> {
             return mConfig;
         });
@@ -249,7 +251,7 @@
         when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig);
         mCarrierConfig.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true);
         mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
-        mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
     }
 
     @Test
@@ -263,7 +265,7 @@
         setupForRequiredProvisioning();
         when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
             .thenReturn(null);
-        mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        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));
@@ -273,7 +275,7 @@
     public void toleratesCarrierConfigMissing() {
         setupForRequiredProvisioning();
         when(mCarrierConfigManager.getConfig()).thenReturn(null);
-        mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
         // We still have a provisioning app configured, so still require provisioning.
         assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
     }
@@ -291,11 +293,11 @@
         setupForRequiredProvisioning();
         when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
             .thenReturn(null);
-        mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
         assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig));
         when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
             .thenReturn(new String[] {"malformedApp"});
-        mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
         assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig));
     }
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
new file mode 100644
index 0000000..ac5c59d
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
@@ -0,0 +1,50 @@
+/*
+ * 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.networkstack.tethering;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.util.SharedLog;
+
+/** FakeTetheringConfiguration is used to override static method for testing. */
+public class FakeTetheringConfiguration extends TetheringConfiguration {
+    FakeTetheringConfiguration(Context ctx, SharedLog log, int id) {
+        super(ctx, log, id);
+    }
+
+    @Override
+    protected String getDeviceConfigProperty(final String name) {
+        return null;
+    }
+
+    @Override
+    protected boolean isFeatureEnabled(Context ctx, String featureVersionFlag) {
+        return false;
+    }
+
+    @Override
+    protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) {
+        return ctx.getResources();
+    }
+
+    @Override
+    protected String getSettingsValue(final String name) {
+        if (mContentResolver == null) return null;
+
+        return super.getSettingsValue(name);
+    }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
index 071a290..4865e03 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java
@@ -25,12 +25,14 @@
 import android.net.ITetheringConnector;
 import android.os.Binder;
 import android.os.IBinder;
+import android.util.ArrayMap;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 public class MockTetheringService extends TetheringService {
     private final Tethering mTethering = mock(Tethering.class);
+    private final ArrayMap<String, Integer> mMockedPermissions = new ArrayMap<>();
 
     @Override
     public IBinder onBind(Intent intent) {
@@ -51,6 +53,15 @@
         return context.checkCallingOrSelfPermission(WRITE_SETTINGS) == PERMISSION_GRANTED;
     }
 
+    @Override
+    public int checkCallingOrSelfPermission(String permission) {
+        final Integer mocked = mMockedPermissions.getOrDefault(permission, null);
+        if (mocked != null) {
+            return mocked;
+        }
+        return super.checkCallingOrSelfPermission(permission);
+    }
+
     public Tethering getTethering() {
         return mTethering;
     }
@@ -68,5 +79,18 @@
         public MockTetheringService getService() {
             return MockTetheringService.this;
         }
+
+        /**
+         * Mock a permission
+         * @param permission Permission to mock
+         * @param granted One of PackageManager.PERMISSION_*, or null to reset to default behavior
+         */
+        public void setPermission(String permission, Integer granted) {
+            if (granted == null) {
+                mMockedPermissions.remove(permission);
+            } else {
+                mMockedPermissions.put(permission, granted);
+            }
+        }
     }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
index df82d7c..6090213 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
@@ -16,6 +16,10 @@
 
 package com.android.networkstack.tethering;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.fail;
 
@@ -64,12 +68,13 @@
     public static final boolean CALLBACKS_FIRST = true;
 
     final Map<NetworkCallback, NetworkRequestInfo> mAllCallbacks = new ArrayMap<>();
+    // This contains the callbacks tracking the system default network, whether it's registered
+    // with registerSystemDefaultNetworkCallback (S+) or with a custom request (R-).
     final Map<NetworkCallback, NetworkRequestInfo> mTrackingDefault = new ArrayMap<>();
     final Map<NetworkCallback, NetworkRequestInfo> mListening = new ArrayMap<>();
     final Map<NetworkCallback, NetworkRequestInfo> mRequested = new ArrayMap<>();
     final Map<NetworkCallback, Integer> mLegacyTypeMap = new ArrayMap<>();
 
-    private final NetworkRequest mDefaultRequest;
     private final Context mContext;
 
     private int mNetworkId = 100;
@@ -80,13 +85,10 @@
      * @param ctx the context to use. Must be a fake or a mock because otherwise the test will
      *            attempt to send real broadcasts and resulting in permission denials.
      * @param svc an IConnectivityManager. Should be a fake or a mock.
-     * @param defaultRequest the default NetworkRequest that will be used by Tethering.
      */
-    public TestConnectivityManager(Context ctx, IConnectivityManager svc,
-            NetworkRequest defaultRequest) {
+    public TestConnectivityManager(Context ctx, IConnectivityManager svc) {
         super(ctx, svc);
         mContext = ctx;
-        mDefaultRequest = defaultRequest;
     }
 
     class NetworkRequestInfo {
@@ -181,11 +183,19 @@
         makeDefaultNetwork(agent, BROADCAST_FIRST, null /* inBetween */);
     }
 
+    static boolean looksLikeDefaultRequest(NetworkRequest req) {
+        return req.hasCapability(NET_CAPABILITY_INTERNET)
+                && !req.hasCapability(NET_CAPABILITY_DUN)
+                && !req.hasTransport(TRANSPORT_CELLULAR);
+    }
+
     @Override
     public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) {
         assertFalse(mAllCallbacks.containsKey(cb));
         mAllCallbacks.put(cb, new NetworkRequestInfo(req, h));
-        if (mDefaultRequest.equals(req)) {
+        // For R- devices, Tethering will invoke this function in 2 cases, one is to request mobile
+        // network, the other is to track system default network.
+        if (looksLikeDefaultRequest(req)) {
             assertFalse(mTrackingDefault.containsKey(cb));
             mTrackingDefault.put(cb, new NetworkRequestInfo(req, h));
         } else {
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 1f4e371..0f940d8 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -26,6 +26,9 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.networkstack.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;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -35,18 +38,23 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ModuleInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.net.util.SharedLog;
 import android.os.Build;
 import android.provider.DeviceConfig;
+import android.provider.Settings;
 import android.telephony.TelephonyManager;
+import android.test.mock.MockContentResolver;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.net.module.util.DeviceConfigUtils;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
@@ -75,16 +83,20 @@
     private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
     private static final String PROVISIONING_APP_RESPONSE = "app_response";
     private static final String TEST_PACKAGE_NAME = "com.android.tethering.test";
+    private static final String APEX_NAME = "com.android.tethering";
     private static final long TEST_PACKAGE_VERSION = 1234L;
+    @Mock private ApplicationInfo mApplicationInfo;
     @Mock private Context mContext;
     @Mock private TelephonyManager mTelephonyManager;
     @Mock private Resources mResources;
     @Mock private Resources mResourcesForSubId;
     @Mock private PackageManager mPackageManager;
+    @Mock private ModuleInfo mMi;
     private Context mMockContext;
     private boolean mHasTelephonyManager;
     private boolean mEnableLegacyDhcpServer;
     private MockitoSession mMockingSession;
+    private MockContentResolver mContentResolver;
 
     private class MockTetheringConfiguration extends TetheringConfiguration {
         MockTetheringConfiguration(Context ctx, SharedLog log, int id) {
@@ -103,6 +115,11 @@
         }
 
         @Override
+        public ApplicationInfo getApplicationInfo() {
+            return mApplicationInfo;
+        }
+
+        @Override
         public Resources getResources() {
             return mResources;
         }
@@ -143,12 +160,15 @@
         final PackageInfo pi = new PackageInfo();
         pi.setLongVersionCode(TEST_PACKAGE_VERSION);
         doReturn(pi).when(mPackageManager).getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt());
+        doReturn(mMi).when(mPackageManager).getModuleInfo(eq(APEX_NAME), anyInt());
+        doReturn(TEST_PACKAGE_NAME).when(mMi).getPackageName();
 
         when(mResources.getStringArray(R.array.config_tether_dhcp_range)).thenReturn(
                 new String[0]);
         when(mResources.getInteger(R.integer.config_tether_offload_poll_interval)).thenReturn(
                 TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
-        when(mResources.getStringArray(R.array.config_tether_usb_regexs)).thenReturn(new String[0]);
+        when(mResources.getStringArray(R.array.config_tether_usb_regexs))
+                .thenReturn(new String[]{ "test_usb\\d" });
         when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
                 .thenReturn(new String[]{ "test_wlan\\d" });
         when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs)).thenReturn(
@@ -166,12 +186,20 @@
         mHasTelephonyManager = true;
         mMockContext = new MockContext(mContext);
         mEnableLegacyDhcpServer = false;
+
+        mContentResolver = new MockContentResolver(mMockContext);
+        mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+        when(mContext.getContentResolver()).thenReturn(mContentResolver);
+        // Call {@link #clearSettingsProvider()} before and after using FakeSettingsProvider.
+        FakeSettingsProvider.clearSettingsProvider();
     }
 
     @After
     public void tearDown() throws Exception {
         mMockingSession.finishMocking();
         DeviceConfigUtils.resetPackageVersionCacheForTest();
+        // Call {@link #clearSettingsProvider()} before and after using FakeSettingsProvider.
+        FakeSettingsProvider.clearSettingsProvider();
     }
 
     private TetheringConfiguration getTetheringConfiguration(int... legacyTetherUpstreamTypes) {
@@ -505,7 +533,7 @@
                 .thenReturn(false);
         setTetherForceUpstreamAutomaticFlagVersion(TEST_PACKAGE_VERSION - 1);
         assertTrue(DeviceConfigUtils.isFeatureEnabled(mMockContext, NAMESPACE_CONNECTIVITY,
-                TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION));
+                TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION, APEX_NAME, false));
 
         assertChooseUpstreamAutomaticallyIs(true);
 
@@ -534,4 +562,42 @@
         assertEquals(value, new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID)
                 .chooseUpstreamAutomatically);
     }
+
+    @Test
+    public void testUsbTetheringFunctions() throws Exception {
+        // Test default value. If both resource and settings is not configured, usingNcm is false.
+        assertIsUsingNcm(false /* usingNcm */);
+
+        when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
+                TETHER_USB_NCM_FUNCTION);
+        assertIsUsingNcm(true /* usingNcm */);
+
+        when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
+                TETHER_USB_RNDIS_FUNCTION);
+        assertIsUsingNcm(false /* usingNcm */);
+
+        setTetherForceUsbFunctions(TETHER_USB_RNDIS_FUNCTION);
+        assertIsUsingNcm(false /* usingNcm */);
+
+        setTetherForceUsbFunctions(TETHER_USB_NCM_FUNCTION);
+        assertIsUsingNcm(true /* usingNcm */);
+
+        // Test throws NumberFormatException.
+        setTetherForceUsbFunctions("WrongNumberFormat");
+        assertIsUsingNcm(false /* usingNcm */);
+    }
+
+    private void assertIsUsingNcm(boolean expected) {
+        final TetheringConfiguration cfg =
+                new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        assertEquals(expected, cfg.isUsingNcm());
+    }
+
+    private void setTetherForceUsbFunctions(final String value) {
+        Settings.Global.putString(mContentResolver, TETHER_FORCE_USB_FUNCTIONS, value);
+    }
+
+    private void setTetherForceUsbFunctions(final int value) {
+        setTetherForceUsbFunctions(Integer.toString(value));
+    }
 }
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 7204ff6..1b52f6e 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -19,13 +19,17 @@
 import static android.Manifest.permission.ACCESS_NETWORK_STATE;
 import static android.Manifest.permission.TETHER_PRIVILEGED;
 import static android.Manifest.permission.WRITE_SETTINGS;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
 import static android.net.TetheringManager.TETHERING_WIFI;
 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 org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -47,6 +51,7 @@
 import androidx.test.rule.ServiceTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.net.module.util.CollectionUtils;
 import com.android.networkstack.tethering.MockTetheringService.MockTetheringConnector;
 
 import org.junit.After;
@@ -67,6 +72,7 @@
     @Rule public ServiceTestRule mServiceTestRule;
     private Tethering mTethering;
     private Intent mMockServiceIntent;
+    private MockTetheringConnector mMockConnector;
     private ITetheringConnector mTetheringConnector;
     private UiAutomation mUiAutomation;
 
@@ -106,10 +112,9 @@
         mMockServiceIntent = new Intent(
                 InstrumentationRegistry.getTargetContext(),
                 MockTetheringService.class);
-        final MockTetheringConnector mockConnector =
-                (MockTetheringConnector) mServiceTestRule.bindService(mMockServiceIntent);
-        mTetheringConnector = mockConnector.getTetheringConnector();
-        final MockTetheringService service = mockConnector.getService();
+        mMockConnector = (MockTetheringConnector) mServiceTestRule.bindService(mMockServiceIntent);
+        mTetheringConnector = mMockConnector.getTetheringConnector();
+        final MockTetheringService service = mMockConnector.getService();
         mTethering = service.getTethering();
     }
 
@@ -141,12 +146,18 @@
 
     private void runTetheringCall(final TestTetheringCall test, String... permissions)
             throws Exception {
+        // Allow the test to run even if ACCESS_NETWORK_STATE was granted at the APK level
+        if (!CollectionUtils.contains(permissions, ACCESS_NETWORK_STATE)) {
+            mMockConnector.setPermission(ACCESS_NETWORK_STATE, PERMISSION_DENIED);
+        }
+
         if (permissions.length > 0) mUiAutomation.adoptShellPermissionIdentity(permissions);
         try {
             when(mTethering.isTetheringSupported()).thenReturn(true);
             test.runTetheringCall(new TestTetheringResult());
         } finally {
             mUiAutomation.dropShellPermissionIdentity();
+            mMockConnector.setPermission(ACCESS_NETWORK_STATE, null);
         }
     }
 
@@ -214,11 +225,15 @@
     }
 
     private void runSetUsbTethering(final TestTetheringResult result) throws Exception {
-        when(mTethering.setUsbTethering(true /* enable */)).thenReturn(TETHER_ERROR_NO_ERROR);
+        doAnswer((invocation) -> {
+            final IIntResultListener listener = invocation.getArgument(1);
+            listener.onResult(TETHER_ERROR_NO_ERROR);
+            return null;
+        }).when(mTethering).setUsbTethering(anyBoolean(), any(IIntResultListener.class));
         mTetheringConnector.setUsbTethering(true /* enable */, TEST_CALLER_PKG,
                 TEST_ATTRIBUTION_TAG, result);
         verify(mTethering).isTetheringSupported();
-        verify(mTethering).setUsbTethering(true /* enable */);
+        verify(mTethering).setUsbTethering(eq(true) /* enable */, any(IIntResultListener.class));
         result.assertResult(TETHER_ERROR_NO_ERROR);
     }
 
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 81ba450..d277e30 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -26,7 +26,6 @@
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
 import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
-import static android.net.ConnectivityManager.TYPE_NONE;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -47,6 +46,7 @@
 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_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
 import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
@@ -68,6 +68,9 @@
 import static com.android.networkstack.tethering.TestConnectivityManager.BROADCAST_FIRST;
 import static com.android.networkstack.tethering.TestConnectivityManager.CALLBACKS_FIRST;
 import static com.android.networkstack.tethering.Tethering.UserRestrictionActionListener;
+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;
 import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
 import static com.android.networkstack.tethering.UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -75,7 +78,6 @@
 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.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.argThat;
@@ -110,6 +112,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
+import android.database.ContentObserver;
 import android.hardware.usb.UsbManager;
 import android.net.ConnectivityManager.NetworkCallback;
 import android.net.EthernetManager;
@@ -134,6 +137,7 @@
 import android.net.TetheredClient.AddressInfo;
 import android.net.TetheringCallbackStartedParcel;
 import android.net.TetheringConfigurationParcel;
+import android.net.TetheringInterface;
 import android.net.TetheringRequestParcel;
 import android.net.dhcp.DhcpLeaseParcelable;
 import android.net.dhcp.DhcpServerCallbacks;
@@ -181,7 +185,6 @@
 import com.android.testutils.MiscAsserts;
 
 import org.junit.After;
-import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -210,7 +213,7 @@
     private static final String TEST_MOBILE_IFNAME = "test_rmnet_data0";
     private static final String TEST_DUN_IFNAME = "test_dun0";
     private static final String TEST_XLAT_MOBILE_IFNAME = "v4-test_rmnet_data0";
-    private static final String TEST_USB_IFNAME = "test_rndis0";
+    private static final String TEST_RNDIS_IFNAME = "test_rndis0";
     private static final String TEST_WIFI_IFNAME = "test_wlan0";
     private static final String TEST_WLAN_IFNAME = "test_wlan1";
     private static final String TEST_P2P_IFNAME = "test_p2p-p2p0-0";
@@ -220,6 +223,11 @@
     private static final String TETHERING_NAME = "Tethering";
     private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
     private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
+    private static final String TEST_RNDIS_REGEX = "test_rndis\\d";
+    private static final String TEST_NCM_REGEX = "test_ncm\\d";
+    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 int CELLULAR_NETID = 100;
     private static final int WIFI_NETID = 101;
@@ -276,8 +284,6 @@
     private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
 
     private TestConnectivityManager mCm;
-    private NetworkRequest mNetworkRequest;
-    private NetworkCallback mDefaultNetworkCallback;
 
     private class TestContext extends BroadcastInterceptingContext {
         TestContext(Context base) {
@@ -344,7 +350,7 @@
         @Override
         public InterfaceParams getInterfaceParams(String ifName) {
             assertTrue("Non-mocked interface " + ifName,
-                    ifName.equals(TEST_USB_IFNAME)
+                    ifName.equals(TEST_RNDIS_IFNAME)
                             || ifName.equals(TEST_WLAN_IFNAME)
                             || ifName.equals(TEST_WIFI_IFNAME)
                             || ifName.equals(TEST_MOBILE_IFNAME)
@@ -354,7 +360,7 @@
                             || ifName.equals(TEST_ETH_IFNAME)
                             || ifName.equals(TEST_BT_IFNAME));
             final String[] ifaces = new String[] {
-                    TEST_USB_IFNAME, TEST_WLAN_IFNAME, TEST_WIFI_IFNAME, TEST_MOBILE_IFNAME,
+                    TEST_RNDIS_IFNAME, TEST_WLAN_IFNAME, TEST_WIFI_IFNAME, TEST_MOBILE_IFNAME,
                     TEST_DUN_IFNAME, TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME};
             return new InterfaceParams(ifName, ArrayUtils.indexOf(ifaces, ifName) + IFINDEX_OFFSET,
                     MacAddress.ALL_ZEROS_ADDRESS);
@@ -378,37 +384,10 @@
         }
     }
 
-    // MyTetheringConfiguration is used to override static method for testing.
-    private class MyTetheringConfiguration extends TetheringConfiguration {
-        MyTetheringConfiguration(Context ctx, SharedLog log, int id) {
-            super(ctx, log, id);
-        }
-
-        @Override
-        protected String getDeviceConfigProperty(final String name) {
-            return null;
-        }
-
-        @Override
-        protected boolean isFeatureEnabled(Context ctx, String featureVersionFlag) {
-            return false;
-        }
-
-        @Override
-        protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) {
-            return mResources;
-        }
-    }
-
     public class MockTetheringDependencies extends TetheringDependencies {
         StateMachine mUpstreamNetworkMonitorSM;
         ArrayList<IpServer> mIpv6CoordinatorNotifyList;
 
-        public void reset() {
-            mUpstreamNetworkMonitorSM = null;
-            mIpv6CoordinatorNotifyList = null;
-        }
-
         @Override
         public BpfCoordinator getBpfCoordinator(
                 BpfCoordinator.Dependencies deps) {
@@ -452,11 +431,6 @@
         }
 
         @Override
-        public NetworkRequest getDefaultNetworkRequest() {
-            return mNetworkRequest;
-        }
-
-        @Override
         public EntitlementManager getEntitlementManager(Context ctx, Handler h, SharedLog log,
                 Runnable callback) {
             mEntitleMgr = spy(super.getEntitlementManager(ctx, h, log, callback));
@@ -471,7 +445,7 @@
         @Override
         public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
                 int subId) {
-            mConfig = spy(new MyTetheringConfiguration(ctx, log, subId));
+            mConfig = spy(new FakeTetheringConfiguration(ctx, log, subId));
             return mConfig;
         }
 
@@ -600,18 +574,13 @@
                 new Network(DUN_NETID));
     }
 
-    // See FakeSettingsProvider#clearSettingsProvider() that this needs to be called before and
-    // after use.
+    // See FakeSettingsProvider#clearSettingsProvider() that this also needs to be called before
+    // use.
     @BeforeClass
     public static void setupOnce() {
         FakeSettingsProvider.clearSettingsProvider();
     }
 
-    @AfterClass
-    public static void tearDownOnce() {
-        FakeSettingsProvider.clearSettingsProvider();
-    }
-
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -621,7 +590,7 @@
                 false);
         when(mNetd.interfaceGetList())
                 .thenReturn(new String[] {
-                        TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_USB_IFNAME, TEST_P2P_IFNAME,
+                        TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_RNDIS_IFNAME, TEST_P2P_IFNAME,
                         TEST_NCM_IFNAME, TEST_ETH_IFNAME, TEST_BT_IFNAME});
         when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
         mInterfaceConfiguration = new InterfaceConfigurationParcel();
@@ -647,15 +616,7 @@
         mServiceContext.registerReceiver(mBroadcastReceiver,
                 new IntentFilter(ACTION_TETHER_STATE_CHANGED));
 
-        // TODO: add NOT_VCN_MANAGED here, but more importantly in the production code.
-        // TODO: even better, change TetheringDependencies.getDefaultNetworkRequest() to use
-        // registerSystemDefaultNetworkCallback() on S and above.
-        NetworkCapabilities defaultCaps = new NetworkCapabilities()
-                .addCapability(NET_CAPABILITY_INTERNET);
-        mNetworkRequest = new NetworkRequest(defaultCaps, TYPE_NONE, 1 /* requestId */,
-                NetworkRequest.Type.REQUEST);
-        mCm = spy(new TestConnectivityManager(mServiceContext, mock(IConnectivityManager.class),
-                mNetworkRequest));
+        mCm = spy(new TestConnectivityManager(mServiceContext, mock(IConnectivityManager.class)));
 
         mTethering = makeTethering();
         verify(mStatsManager, times(1)).registerNetworkStatsProvider(anyString(), any());
@@ -682,17 +643,19 @@
                 supported ? 1 : 0);
         when(mUserManager.hasUserRestriction(
                 UserManager.DISALLOW_CONFIG_TETHERING)).thenReturn(!supported);
+        when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
+                TetheringConfiguration.TETHER_USB_RNDIS_FUNCTION);
         // Setup tetherable configuration.
         when(mResources.getStringArray(R.array.config_tether_usb_regexs))
-                .thenReturn(new String[] { "test_rndis\\d" });
+                .thenReturn(new String[] { TEST_RNDIS_REGEX});
         when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
-                .thenReturn(new String[] { "test_wlan\\d" });
+                .thenReturn(new String[] { TEST_WIFI_REGEX });
         when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs))
-                .thenReturn(new String[] { "test_p2p-p2p\\d-.*" });
+                .thenReturn(new String[] { TEST_P2P_REGEX });
         when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
-                .thenReturn(new String[] { "test_pan\\d" });
+                .thenReturn(new String[] { TEST_BT_REGEX });
         when(mResources.getStringArray(R.array.config_tether_ncm_regexs))
-                .thenReturn(new String[] { "test_ncm\\d" });
+                .thenReturn(new String[] { TEST_NCM_REGEX });
         when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(
                 new int[] { TYPE_WIFI, TYPE_MOBILE_DUN });
         when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true);
@@ -704,7 +667,6 @@
     }
 
     private Tethering makeTethering() {
-        mTetheringDependencies.reset();
         return new Tethering(mTetheringDependencies);
     }
 
@@ -729,6 +691,7 @@
     @After
     public void tearDown() {
         mServiceContext.unregisterReceiver(mBroadcastReceiver);
+        FakeSettingsProvider.clearSettingsProvider();
     }
 
     private void sendWifiApStateChanged(int state) {
@@ -774,16 +737,18 @@
         mLooper.dispatchAll();
     }
 
-    private void sendUsbBroadcast(boolean connected, boolean configured, boolean function,
-            int type) {
+    private boolean tetherUsbFunctionMatches(int function, int enabledType) {
+        return function == enabledType;
+    }
+
+    private void sendUsbBroadcast(boolean connected, boolean configured, int function) {
         final Intent intent = new Intent(UsbManager.ACTION_USB_STATE);
         intent.putExtra(USB_CONNECTED, connected);
         intent.putExtra(USB_CONFIGURED, configured);
-        if (type == TETHERING_USB) {
-            intent.putExtra(USB_FUNCTION_RNDIS, function);
-        } else {
-            intent.putExtra(USB_FUNCTION_NCM, function);
-        }
+        intent.putExtra(USB_FUNCTION_RNDIS,
+                tetherUsbFunctionMatches(TETHER_USB_RNDIS_FUNCTION, function));
+        intent.putExtra(USB_FUNCTION_NCM,
+                tetherUsbFunctionMatches(TETHER_USB_NCM_FUNCTION, function));
         mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
         mLooper.dispatchAll();
     }
@@ -795,16 +760,12 @@
     }
 
     private void verifyDefaultNetworkRequestFiled() {
-        ArgumentCaptor<NetworkCallback> captor = ArgumentCaptor.forClass(NetworkCallback.class);
-        verify(mCm, times(1)).requestNetwork(eq(mNetworkRequest),
-                captor.capture(), any(Handler.class));
-        mDefaultNetworkCallback = captor.getValue();
-        assertNotNull(mDefaultNetworkCallback);
-
+        ArgumentCaptor<NetworkRequest> reqCaptor = ArgumentCaptor.forClass(NetworkRequest.class);
+        verify(mCm, times(1)).requestNetwork(reqCaptor.capture(),
+                any(NetworkCallback.class), any(Handler.class));
+        assertTrue(TestConnectivityManager.looksLikeDefaultRequest(reqCaptor.getValue()));
         // The default network request is only ever filed once.
         verifyNoMoreInteractions(mCm);
-        mUpstreamNetworkMonitor.startTrackDefaultNetwork(mNetworkRequest, mEntitleMgr);
-        verifyNoMoreInteractions(mCm);
     }
 
     private void verifyInterfaceServingModeStarted(String ifname) throws Exception {
@@ -862,11 +823,18 @@
         final TetheringRequestParcel request = createTetheringRequestParcel(TETHERING_USB);
         mTethering.startTethering(request, null);
         mLooper.dispatchAll();
-        verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+
         assertEquals(1, mTethering.getActiveTetheringRequests().size());
         assertEquals(request, mTethering.getActiveTetheringRequests().get(TETHERING_USB));
 
-        mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
+        if (mTethering.getTetheringConfiguration().isUsingNcm()) {
+            verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NCM);
+            mTethering.interfaceStatusChanged(TEST_NCM_IFNAME, true);
+        } else {
+            verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+            mTethering.interfaceStatusChanged(TEST_RNDIS_IFNAME, true);
+        }
+
     }
 
     @Test
@@ -879,7 +847,7 @@
         verifyNoMoreInteractions(mNetd);
 
         // Pretend we then receive USB configured broadcast.
-        sendUsbBroadcast(true, true, true, TETHERING_USB);
+        sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
         // Now we should see the start of tethering mechanics (in this case:
         // tetherMatchingInterfaces() which starts by fetching all interfaces).
         verify(mNetd, times(1)).interfaceGetList();
@@ -943,7 +911,7 @@
         verifyNoMoreInteractions(mWifiManager);
         // Asking for the last error after the per-interface state machine
         // has been reaped yields an unknown interface error.
-        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_WLAN_IFNAME));
+        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_WLAN_IFNAME));
     }
 
     /**
@@ -951,16 +919,13 @@
      */
     private void sendIPv6TetherUpdates(UpstreamNetworkState upstreamState) {
         // IPv6TetheringCoordinator must have been notified of downstream
-        verify(mIPv6TetheringCoordinator, times(1)).addActiveDownstream(
-                argThat(sm -> sm.linkProperties().getInterfaceName().equals(TEST_USB_IFNAME)),
-                eq(IpServer.STATE_TETHERED));
-
         for (IpServer ipSrv : mTetheringDependencies.mIpv6CoordinatorNotifyList) {
             UpstreamNetworkState ipv6OnlyState = buildMobileUpstreamState(false, true, false);
             ipSrv.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0,
                     upstreamState.linkProperties.isIpv6Provisioned()
                             ? ipv6OnlyState.linkProperties
                             : null);
+            break;
         }
         mLooper.dispatchAll();
     }
@@ -968,7 +933,18 @@
     private void runUsbTethering(UpstreamNetworkState upstreamState) {
         initTetheringUpstream(upstreamState);
         prepareUsbTethering();
-        sendUsbBroadcast(true, true, true, TETHERING_USB);
+        if (mTethering.getTetheringConfiguration().isUsingNcm()) {
+            sendUsbBroadcast(true, true, TETHER_USB_NCM_FUNCTION);
+            verify(mIPv6TetheringCoordinator).addActiveDownstream(
+                    argThat(sm -> sm.linkProperties().getInterfaceName().equals(TEST_NCM_IFNAME)),
+                    eq(IpServer.STATE_TETHERED));
+        } else {
+            sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
+            verify(mIPv6TetheringCoordinator).addActiveDownstream(
+                    argThat(sm -> sm.linkProperties().getInterfaceName().equals(TEST_RNDIS_IFNAME)),
+                    eq(IpServer.STATE_TETHERED));
+        }
+
     }
 
     private void assertSetIfaceToDadProxy(final int numOfCalls, final String ifaceName) {
@@ -984,8 +960,8 @@
         UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
         assertSetIfaceToDadProxy(0 /* numOfCalls */, "" /* ifaceName */);
@@ -1011,8 +987,8 @@
         UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
         // TODO: add interfaceParams to compare in verify.
@@ -1026,8 +1002,8 @@
         UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
         verify(mRouterAdvertisementDaemon, times(1)).start();
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
@@ -1043,12 +1019,13 @@
         UpstreamNetworkState upstreamState = buildMobile464xlatUpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
-        verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+        verify(mNetd, times(1)).tetherAddForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME,
+                TEST_XLAT_MOBILE_IFNAME);
+        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_RNDIS_IFNAME, TEST_MOBILE_IFNAME);
 
         sendIPv6TetherUpdates(upstreamState);
         assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */);
@@ -1058,14 +1035,20 @@
 
     @Test
     public void workingMobileUsbTethering_v6Then464xlat() throws Exception {
+        when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
+                TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
+        when(mResources.getStringArray(R.array.config_tether_usb_regexs))
+                .thenReturn(new String[] {TEST_NCM_REGEX});
+        sendConfigurationChanged();
+
         // Setup IPv6
         UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
         runUsbTethering(upstreamState);
 
-        verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
 
         // Then 464xlat comes up
         upstreamState = buildMobile464xlatUpstreamState();
@@ -1080,11 +1063,12 @@
         mLooper.dispatchAll();
 
         // Forwarding is added for 464xlat
-        verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+        verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_XLAT_MOBILE_IFNAME);
+        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME,
+                TEST_XLAT_MOBILE_IFNAME);
         // Forwarding was not re-added for v6 (still times(1))
-        verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd, times(1)).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
         // DHCP not restarted on downstream (still times(1))
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
@@ -1107,6 +1091,11 @@
         verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network);
     }
 
+    private void verifyDisableTryCellWhenTetheringStop(InOrder inOrder) {
+        runStopUSBTethering();
+        inOrder.verify(mUpstreamNetworkMonitor).setTryCell(false);
+    }
+
     private void upstreamSelectionTestCommon(final boolean automatic, InOrder inOrder,
             TestNetworkAgent mobile, TestNetworkAgent wifi) throws Exception {
         // Enable automatic upstream selection.
@@ -1116,7 +1105,7 @@
 
         // Start USB tethering with no current upstream.
         prepareUsbTethering();
-        sendUsbBroadcast(true, true, true, TETHERING_USB);
+        sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
         inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
         inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
 
@@ -1130,6 +1119,7 @@
         wifi.fakeConnect();
         mCm.makeDefaultNetwork(wifi, BROADCAST_FIRST);
         mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setTryCell(false);
         inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId);
     }
 
@@ -1152,6 +1142,7 @@
 
         mCm.makeDefaultNetwork(wifi, BROADCAST_FIRST, doDispatchAll);
         mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setTryCell(false);
         inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId);
 
         mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST);
@@ -1160,6 +1151,7 @@
 
         mCm.makeDefaultNetwork(wifi, CALLBACKS_FIRST);
         mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setTryCell(false);
         inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId);
 
         mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST, doDispatchAll);
@@ -1178,6 +1170,7 @@
         mLooper.dispatchAll();
         mobile.fakeDisconnect();
         mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
         inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null);
 
         mobile = new TestNetworkAgent(mCm, buildMobile464xlatUpstreamState());
@@ -1195,6 +1188,7 @@
         mCm.makeDefaultNetwork(null, CALLBACKS_FIRST, doDispatchAll);
         mobile.fakeDisconnect();
         mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
         inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null);
 
         mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
@@ -1210,6 +1204,8 @@
         mobile.fakeDisconnect();
         mobile.sendLinkProperties();
         mLooper.dispatchAll();
+
+        verifyDisableTryCellWhenTetheringStop(inOrder);
     }
 
     @Test
@@ -1232,6 +1228,10 @@
         mLooper.dispatchAll();
         mCm.makeDefaultNetwork(wifi, CALLBACKS_FIRST, null);
         mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setTryCell(false);
+        inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId);
+
+        verifyDisableTryCellWhenTetheringStop(inOrder);
     }
 
     @Test
@@ -1247,6 +1247,7 @@
         // automatic mode would request dun again and choose it as upstream.
         mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST);
         mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
         ArgumentCaptor<NetworkCallback> captor = ArgumentCaptor.forClass(NetworkCallback.class);
         inOrder.verify(mCm).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(), any());
         inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null);
@@ -1258,11 +1259,14 @@
         // Lose and regain upstream again.
         dun.fakeDisconnect(CALLBACKS_FIRST, doDispatchAll);
         mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
         inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null);
         inOrder.verify(mCm, never()).unregisterNetworkCallback(any(NetworkCallback.class));
         dun.fakeConnect(CALLBACKS_FIRST, doDispatchAll);
         mLooper.dispatchAll();
         inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(dun.networkId);
+
+        verifyDisableTryCellWhenTetheringStop(inOrder);
     }
 
     @Test
@@ -1278,6 +1282,7 @@
         // list).
         mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST);
         mLooper.dispatchAll();
+        inOrder.verify(mUpstreamNetworkMonitor).setTryCell(false);
         inOrder.verify(mUpstreamNetworkMonitor, never()).setCurrentUpstream(any());
         // BUG: when wifi disconnect, the dun request would not be filed again because wifi is
         // no longer be default network which do not have CONNECTIVIY_ACTION broadcast.
@@ -1299,8 +1304,11 @@
         mLooper.dispatchAll();
         // BUG: dun has higher priority than wifi but tethering don't file dun request because
         // current upstream is wifi.
+        inOrder.verify(mUpstreamNetworkMonitor).setTryCell(false);
         inOrder.verify(mCm, never()).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(),
                 any());
+
+        verifyDisableTryCellWhenTetheringStop(inOrder);
     }
 
     private void chooseDunUpstreamTestCommon(final boolean automatic, InOrder inOrder,
@@ -1312,7 +1320,7 @@
 
         // Start USB tethering with no current upstream.
         prepareUsbTethering();
-        sendUsbBroadcast(true, true, true, TETHERING_USB);
+        sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
         inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
         inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
         ArgumentCaptor<NetworkCallback> captor = ArgumentCaptor.forClass(NetworkCallback.class);
@@ -1346,7 +1354,7 @@
 
     private void runNcmTethering() {
         prepareNcmTethering();
-        sendUsbBroadcast(true, true, true, TETHERING_NCM);
+        sendUsbBroadcast(true, true, TETHER_USB_NCM_FUNCTION);
     }
 
     @Test
@@ -1476,7 +1484,7 @@
         verifyNoMoreInteractions(mWifiManager);
         // Asking for the last error after the per-interface state machine
         // has been reaped yields an unknown interface error.
-        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_WLAN_IFNAME));
+        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_WLAN_IFNAME));
     }
 
     // TODO: Test with and without interfaceStatusChanged().
@@ -1626,7 +1634,7 @@
         // Start usb tethering and check that usb interface is tethered.
         final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         runUsbTethering(upstreamState);
-        assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME);
+        assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_RNDIS_IFNAME);
         assertTrue(mTethering.isTetheringActive());
         assertEquals(0, mTethering.getActiveTetheringRequests().size());
 
@@ -1798,6 +1806,8 @@
     public void testRegisterTetheringEventCallback() throws Exception {
         TestTetheringEventCallback callback = new TestTetheringEventCallback();
         TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
+        final TetheringInterface wifiIface = new TetheringInterface(
+                TETHERING_WIFI, TEST_WLAN_IFNAME);
 
         // 1. Register one callback before running any tethering.
         mTethering.registerTetheringEventCallback(callback);
@@ -1816,12 +1826,12 @@
         mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
         mLooper.dispatchAll();
         tetherState = callback.pollTetherStatesChanged();
-        assertArrayEquals(tetherState.availableList, new String[] {TEST_WLAN_IFNAME});
+        assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
 
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
         sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
         tetherState = callback.pollTetherStatesChanged();
-        assertArrayEquals(tetherState.tetheredList, new String[] {TEST_WLAN_IFNAME});
+        assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface});
         callback.expectUpstreamChanged(upstreamState.network);
         callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STARTED);
 
@@ -1833,7 +1843,7 @@
         callback2.expectConfigurationChanged(
                 mTethering.getTetheringConfiguration().toStableParcelable());
         tetherState = callback2.pollTetherStatesChanged();
-        assertEquals(tetherState.tetheredList, new String[] {TEST_WLAN_IFNAME});
+        assertEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface});
         callback2.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STARTED);
 
         // 4. Unregister first callback and disable wifi tethering
@@ -1842,7 +1852,7 @@
         mTethering.stopTethering(TETHERING_WIFI);
         sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
         tetherState = callback2.pollTetherStatesChanged();
-        assertArrayEquals(tetherState.availableList, new String[] {TEST_WLAN_IFNAME});
+        assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
         mLooper.dispatchAll();
         callback2.expectUpstreamChanged(new Network[] {null});
         callback2.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
@@ -1864,7 +1874,7 @@
         callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_FAILED);
         runStopUSBTethering();
         callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
-        reset(mUsbManager);
+        reset(mUsbManager, mIPv6TetheringCoordinator);
         // 2. Offload fail if no OffloadControl.
         initOffloadConfiguration(true /* offloadConfig */, OFFLOAD_HAL_VERSION_NONE,
                 0 /* defaultDisabled */);
@@ -1872,7 +1882,7 @@
         callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_FAILED);
         runStopUSBTethering();
         callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
-        reset(mUsbManager);
+        reset(mUsbManager, mIPv6TetheringCoordinator);
         // 3. Offload fail if disabled by settings.
         initOffloadConfiguration(true /* offloadConfig */, OFFLOAD_HAL_VERSION_1_0,
                 1 /* defaultDisabled */);
@@ -1885,8 +1895,10 @@
     private void runStopUSBTethering() {
         mTethering.stopTethering(TETHERING_USB);
         mLooper.dispatchAll();
-        mTethering.interfaceRemoved(TEST_USB_IFNAME);
+        mTethering.interfaceRemoved(TEST_RNDIS_IFNAME);
+        sendUsbBroadcast(true, true, -1 /* function */);
         mLooper.dispatchAll();
+        verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
     }
 
     private void initOffloadConfiguration(final boolean offloadConfig,
@@ -1945,7 +1957,7 @@
         // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY
         verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE);
 
-        assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastTetherError(TEST_P2P_IFNAME));
+        assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
 
         // Emulate externally-visible WifiP2pManager effects, when wifi p2p group
         // is being removed.
@@ -1964,7 +1976,7 @@
         verifyNoMoreInteractions(mNetd);
         // Asking for the last error after the per-interface state machine
         // has been reaped yields an unknown interface error.
-        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_P2P_IFNAME));
+        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
     }
 
     private void workingWifiP2pGroupClient(
@@ -1994,7 +2006,7 @@
         verifyNoMoreInteractions(mNetd);
         // Asking for the last error after the per-interface state machine
         // has been reaped yields an unknown interface error.
-        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_P2P_IFNAME));
+        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
     }
 
     @Test
@@ -2025,7 +2037,7 @@
         verify(mNetd, never()).networkAddInterface(INetd.LOCAL_NET_ID, TEST_P2P_IFNAME);
         verify(mNetd, never()).ipfwdEnableForwarding(TETHERING_NAME);
         verify(mNetd, never()).tetherStartWithConfiguration(any());
-        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastTetherError(TEST_P2P_IFNAME));
+        assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
     }
     @Test
     public void workingWifiP2pGroupOwnerLegacyModeWithIfaceChanged() throws Exception {
@@ -2062,29 +2074,29 @@
         // Start Tethering.
         final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
         runUsbTethering(upstreamState);
-        assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME);
+        assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_RNDIS_IFNAME);
         // Data saver is ON.
         setDataSaverEnabled(true);
         // Verify that tethering should be disabled.
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
-        mTethering.interfaceRemoved(TEST_USB_IFNAME);
+        mTethering.interfaceRemoved(TEST_RNDIS_IFNAME);
         mLooper.dispatchAll();
         assertEquals(mTethering.getTetheredIfaces(), new String[0]);
-        reset(mUsbManager);
+        reset(mUsbManager, mIPv6TetheringCoordinator);
 
         runUsbTethering(upstreamState);
         // Verify that user can start tethering again without turning OFF data saver.
-        assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME);
+        assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_RNDIS_IFNAME);
 
         // If data saver is keep ON with change event, tethering should not be OFF this time.
         setDataSaverEnabled(true);
         verify(mUsbManager, times(0)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
-        assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME);
+        assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_RNDIS_IFNAME);
 
         // If data saver is turned OFF, it should not change tethering.
         setDataSaverEnabled(false);
         verify(mUsbManager, times(0)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
-        assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME);
+        assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_RNDIS_IFNAME);
     }
 
     private static <T> void assertContains(Collection<T> collection, T element) {
@@ -2144,8 +2156,8 @@
         verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
 
         // Expect that when USB comes up, the DHCP server is configured with the requested address.
-        mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
-        sendUsbBroadcast(true, true, true, TETHERING_USB);
+        mTethering.interfaceStatusChanged(TEST_RNDIS_IFNAME, true);
+        sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
         verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
                 any(), any());
         verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr)));
@@ -2153,6 +2165,12 @@
 
     @Test
     public void testRequestStaticIp() throws Exception {
+        when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
+                TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
+        when(mResources.getStringArray(R.array.config_tether_usb_regexs))
+                .thenReturn(new String[] {TEST_NCM_REGEX});
+        sendConfigurationChanged();
+
         final LinkAddress serverLinkAddr = new LinkAddress("192.168.0.123/24");
         final LinkAddress clientLinkAddr = new LinkAddress("192.168.0.42/24");
         final String serverAddr = "192.168.0.123";
@@ -2162,9 +2180,9 @@
         mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
                   serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), null);
         mLooper.dispatchAll();
-        verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
-        mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
-        sendUsbBroadcast(true, true, true, TETHERING_USB);
+        verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
+        mTethering.interfaceStatusChanged(TEST_NCM_IFNAME, true);
+        sendUsbBroadcast(true, true, TETHER_USB_NCM_FUNCTION);
         verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr)));
         verify(mIpServerDependencies, times(1)).makeDhcpServer(any(), dhcpParamsCaptor.capture(),
                 any());
@@ -2331,7 +2349,7 @@
                 wifiNetwork, TEST_WIFI_IFNAME, TRANSPORT_WIFI);
         // verify turn off usb tethering
         verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
-        mTethering.interfaceRemoved(TEST_USB_IFNAME);
+        mTethering.interfaceRemoved(TEST_RNDIS_IFNAME);
         mLooper.dispatchAll();
         // verify restart usb tethering
         verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
@@ -2372,7 +2390,7 @@
         verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
         // verify turn off ethernet tethering
         verify(mockRequest).release();
-        mTethering.interfaceRemoved(TEST_USB_IFNAME);
+        mTethering.interfaceRemoved(TEST_RNDIS_IFNAME);
         ethCallback.onUnavailable();
         mLooper.dispatchAll();
         // verify restart usb tethering
@@ -2385,15 +2403,16 @@
         reset(mUsbManager, mEm);
         when(mNetd.interfaceGetList())
                 .thenReturn(new String[] {
-                        TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_USB_IFNAME, TEST_P2P_IFNAME,
+                        TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_RNDIS_IFNAME, TEST_P2P_IFNAME,
                         TEST_NCM_IFNAME, TEST_ETH_IFNAME});
 
-        mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
-        sendUsbBroadcast(true, true, true, TETHERING_USB);
-        assertContains(Arrays.asList(mTethering.getTetherableIfaces()), TEST_USB_IFNAME);
-        assertContains(Arrays.asList(mTethering.getTetherableIfaces()), TEST_ETH_IFNAME);
-        assertEquals(TETHER_ERROR_IFACE_CFG_ERROR, mTethering.getLastTetherError(TEST_USB_IFNAME));
-        assertEquals(TETHER_ERROR_IFACE_CFG_ERROR, mTethering.getLastTetherError(TEST_ETH_IFNAME));
+        mTethering.interfaceStatusChanged(TEST_RNDIS_IFNAME, true);
+        sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
+        assertContains(Arrays.asList(mTethering.getTetherableIfacesForTest()), TEST_RNDIS_IFNAME);
+        assertContains(Arrays.asList(mTethering.getTetherableIfacesForTest()), TEST_ETH_IFNAME);
+        assertEquals(TETHER_ERROR_IFACE_CFG_ERROR, mTethering.getLastErrorForTest(
+                TEST_RNDIS_IFNAME));
+        assertEquals(TETHER_ERROR_IFACE_CFG_ERROR, mTethering.getLastErrorForTest(TEST_ETH_IFNAME));
     }
 
     @Test
@@ -2583,6 +2602,46 @@
         reset(mBluetoothAdapter, mBluetoothPan);
     }
 
+    @Test
+    public void testUsbTetheringWithNcmFunction() throws Exception {
+        when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
+                TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
+        when(mResources.getStringArray(R.array.config_tether_usb_regexs))
+                .thenReturn(new String[] {TEST_NCM_REGEX});
+        sendConfigurationChanged();
+
+        // 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);
+        mLooper.dispatchAll();
+        ncmResult.assertHasResult();
+
+        final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
+        runUsbTethering(upstreamState);
+
+        verify(mNetd).interfaceGetList();
+        verify(mNetd).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+        verify(mNetd).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+
+        verify(mRouterAdvertisementDaemon).start();
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
+                any(), any());
+        sendIPv6TetherUpdates(upstreamState);
+        assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */);
+        verify(mRouterAdvertisementDaemon).buildNewRa(any(), notNull());
+        verify(mNetd).tetherApplyDnsInterfaces();
+
+        Settings.Global.putInt(mContentResolver, TETHER_FORCE_USB_FUNCTIONS,
+                TETHER_USB_RNDIS_FUNCTION);
+        final ContentObserver observer = mTethering.getSettingsObserverForTest();
+        observer.onChange(false /* selfChange */);
+        mLooper.dispatchAll();
+        // stop TETHERING_USB and TETHERING_NCM
+        verify(mUsbManager, times(2)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+        mTethering.interfaceRemoved(TEST_NCM_IFNAME);
+        sendUsbBroadcast(true, true, -1 /* function */);
+    }
     // TODO: Test that a request for hotspot mode doesn't interfere with an
     // already operating tethering mode interface.
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index cee0365..ce4ba85 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -29,10 +29,10 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
 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.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -66,6 +66,7 @@
 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;
 
@@ -83,10 +84,6 @@
     private static final boolean INCLUDES = true;
     private static final boolean EXCLUDES = false;
 
-    // Actual contents of the request don't matter for this test. The lack of
-    // any specific TRANSPORT_* is sufficient to identify this request.
-    private static final NetworkRequest sDefaultRequest = new NetworkRequest.Builder().build();
-
     private static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities.Builder()
             .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_INTERNET).build();
     private static final NetworkCapabilities DUN_CAPABILITIES = new NetworkCapabilities.Builder()
@@ -113,9 +110,10 @@
         when(mLog.forSubComponent(anyString())).thenReturn(mLog);
         when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
 
-        mCM = spy(new TestConnectivityManager(mContext, mCS, sDefaultRequest));
+        mCM = spy(new TestConnectivityManager(mContext, mCS));
+        when(mContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))).thenReturn(mCM);
         mSM = new TestStateMachine(mLooper.getLooper());
-        mUNM = new UpstreamNetworkMonitor(mCM, mSM, mLog, EVENT_UNM_UPDATE);
+        mUNM = new UpstreamNetworkMonitor(mContext, mSM, mLog, EVENT_UNM_UPDATE);
     }
 
     @After public void tearDown() throws Exception {
@@ -146,7 +144,7 @@
     @Test
     public void testDefaultNetworkIsTracked() throws Exception {
         assertTrue(mCM.hasNoCallbacks());
-        mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr);
+        mUNM.startTrackDefaultNetwork(mEntitleMgr);
 
         mUNM.startObserveAllNetworks();
         assertEquals(1, mCM.mTrackingDefault.size());
@@ -159,7 +157,7 @@
     public void testListensForAllNetworks() throws Exception {
         assertTrue(mCM.mListening.isEmpty());
 
-        mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr);
+        mUNM.startTrackDefaultNetwork(mEntitleMgr);
         mUNM.startObserveAllNetworks();
         assertFalse(mCM.mListening.isEmpty());
         assertTrue(mCM.isListeningForAll());
@@ -170,9 +168,17 @@
 
     @Test
     public void testCallbacksRegistered() {
-        mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr);
+        mUNM.startTrackDefaultNetwork(mEntitleMgr);
+        // Verify the fired default request matches expectation.
+        final ArgumentCaptor<NetworkRequest> requestCaptor =
+                ArgumentCaptor.forClass(NetworkRequest.class);
         verify(mCM, times(1)).requestNetwork(
-                eq(sDefaultRequest), any(NetworkCallback.class), any(Handler.class));
+                requestCaptor.capture(), any(NetworkCallback.class), any(Handler.class));
+        // For R- devices, Tethering will invoke this function in 2 cases, one is to
+        // request mobile network, the other is to track system default network. Verify
+        // the request is the one tracks default network.
+        assertTrue(TestConnectivityManager.looksLikeDefaultRequest(requestCaptor.getValue()));
+
         mUNM.startObserveAllNetworks();
         verify(mCM, times(1)).registerNetworkCallback(
                 any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
@@ -293,7 +299,7 @@
         final Collection<Integer> preferredTypes = new ArrayList<>();
         preferredTypes.add(TYPE_WIFI);
 
-        mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr);
+        mUNM.startTrackDefaultNetwork(mEntitleMgr);
         mUNM.startObserveAllNetworks();
         // There are no networks, so there is nothing to select.
         assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
@@ -366,7 +372,7 @@
 
     @Test
     public void testGetCurrentPreferredUpstream() throws Exception {
-        mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr);
+        mUNM.startTrackDefaultNetwork(mEntitleMgr);
         mUNM.startObserveAllNetworks();
         mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
         mUNM.setTryCell(true);
@@ -438,7 +444,7 @@
 
     @Test
     public void testLocalPrefixes() throws Exception {
-        mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr);
+        mUNM.startTrackDefaultNetwork(mEntitleMgr);
         mUNM.startObserveAllNetworks();
 
         // [0] Test minimum set of local prefixes.
@@ -549,7 +555,7 @@
         // Mobile has higher pirority than wifi.
         preferredTypes.add(TYPE_MOBILE_HIPRI);
         preferredTypes.add(TYPE_WIFI);
-        mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr);
+        mUNM.startTrackDefaultNetwork(mEntitleMgr);
         mUNM.startObserveAllNetworks();
         // Setup wifi and make wifi as default network.
         final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
new file mode 100644
index 0000000..ff90e78
--- /dev/null
+++ b/core/java/android/net/ConnectivityManager.java
@@ -0,0 +1,2416 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.net.NetworkUtils;
+import android.os.Binder;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.INetworkActivityListener;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.util.Protocol;
+
+import java.net.InetAddress;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.HashMap;
+
+/**
+ * Class that answers queries about the state of network connectivity. It also
+ * notifies applications when network connectivity changes. Get an instance
+ * of this class by calling
+ * {@link android.content.Context#getSystemService(String) Context.getSystemService(Context.CONNECTIVITY_SERVICE)}.
+ * <p>
+ * The primary responsibilities of this class are to:
+ * <ol>
+ * <li>Monitor network connections (Wi-Fi, GPRS, UMTS, etc.)</li>
+ * <li>Send broadcast intents when network connectivity changes</li>
+ * <li>Attempt to "fail over" to another network when connectivity to a network
+ * is lost</li>
+ * <li>Provide an API that allows applications to query the coarse-grained or fine-grained
+ * state of the available networks</li>
+ * <li>Provide an API that allows applications to request and select networks for their data
+ * traffic</li>
+ * </ol>
+ */
+public class ConnectivityManager {
+    private static final String TAG = "ConnectivityManager";
+
+    /**
+     * A change in network connectivity has occurred. A default connection has either
+     * been established or lost. The NetworkInfo for the affected network is
+     * sent as an extra; it should be consulted to see what kind of
+     * connectivity event occurred.
+     * <p/>
+     * If this is a connection that was the result of failing over from a
+     * disconnected network, then the FAILOVER_CONNECTION boolean extra is
+     * set to true.
+     * <p/>
+     * For a loss of connectivity, if the connectivity manager is attempting
+     * to connect (or has already connected) to another network, the
+     * NetworkInfo for the new network is also passed as an extra. This lets
+     * any receivers of the broadcast know that they should not necessarily
+     * tell the user that no data traffic will be possible. Instead, the
+     * receiver should expect another broadcast soon, indicating either that
+     * the failover attempt succeeded (and so there is still overall data
+     * connectivity), or that the failover attempt failed, meaning that all
+     * connectivity has been lost.
+     * <p/>
+     * For a disconnect event, the boolean extra EXTRA_NO_CONNECTIVITY
+     * is set to {@code true} if there are no connected networks at all.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
+
+    /**
+     * Identical to {@link #CONNECTIVITY_ACTION} broadcast, but sent without any
+     * applicable {@link Settings.Global#CONNECTIVITY_CHANGE_DELAY}.
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String CONNECTIVITY_ACTION_IMMEDIATE =
+            "android.net.conn.CONNECTIVITY_CHANGE_IMMEDIATE";
+
+    /**
+     * The lookup key for a {@link NetworkInfo} object. Retrieve with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     *
+     * @deprecated Since {@link NetworkInfo} can vary based on UID, applications
+     *             should always obtain network information through
+     *             {@link #getActiveNetworkInfo()} or
+     *             {@link #getAllNetworkInfo()}.
+     * @see #EXTRA_NETWORK_TYPE
+     */
+    @Deprecated
+    public static final String EXTRA_NETWORK_INFO = "networkInfo";
+
+    /**
+     * Network type which triggered a {@link #CONNECTIVITY_ACTION} broadcast.
+     * Can be used with {@link #getNetworkInfo(int)} to get {@link NetworkInfo}
+     * state based on the calling application.
+     *
+     * @see android.content.Intent#getIntExtra(String, int)
+     */
+    public static final String EXTRA_NETWORK_TYPE = "networkType";
+
+    /**
+     * The lookup key for a boolean that indicates whether a connect event
+     * is for a network to which the connectivity manager was failing over
+     * following a disconnect on another network.
+     * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+     */
+    public static final String EXTRA_IS_FAILOVER = "isFailover";
+    /**
+     * The lookup key for a {@link NetworkInfo} object. This is supplied when
+     * there is another network that it may be possible to connect to. Retrieve with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     */
+    public static final String EXTRA_OTHER_NETWORK_INFO = "otherNetwork";
+    /**
+     * The lookup key for a boolean that indicates whether there is a
+     * complete lack of connectivity, i.e., no network is available.
+     * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+     */
+    public static final String EXTRA_NO_CONNECTIVITY = "noConnectivity";
+    /**
+     * The lookup key for a string that indicates why an attempt to connect
+     * to a network failed. The string has no particular structure. It is
+     * intended to be used in notifications presented to users. Retrieve
+     * it with {@link android.content.Intent#getStringExtra(String)}.
+     */
+    public static final String EXTRA_REASON = "reason";
+    /**
+     * The lookup key for a string that provides optionally supplied
+     * extra information about the network state. The information
+     * may be passed up from the lower networking layers, and its
+     * meaning may be specific to a particular network type. Retrieve
+     * it with {@link android.content.Intent#getStringExtra(String)}.
+     */
+    public static final String EXTRA_EXTRA_INFO = "extraInfo";
+    /**
+     * The lookup key for an int that provides information about
+     * our connection to the internet at large.  0 indicates no connection,
+     * 100 indicates a great connection.  Retrieve it with
+     * {@link android.content.Intent#getIntExtra(String, int)}.
+     * {@hide}
+     */
+    public static final String EXTRA_INET_CONDITION = "inetCondition";
+
+    /**
+     * Broadcast action to indicate the change of data activity status
+     * (idle or active) on a network in a recent period.
+     * The network becomes active when data transmission is started, or
+     * idle if there is no data transmission for a period of time.
+     * {@hide}
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_DATA_ACTIVITY_CHANGE = "android.net.conn.DATA_ACTIVITY_CHANGE";
+    /**
+     * The lookup key for an enum that indicates the network device type on which this data activity
+     * change happens.
+     * {@hide}
+     */
+    public static final String EXTRA_DEVICE_TYPE = "deviceType";
+    /**
+     * The lookup key for a boolean that indicates the device is active or not. {@code true} means
+     * it is actively sending or receiving data and {@code false} means it is idle.
+     * {@hide}
+     */
+    public static final String EXTRA_IS_ACTIVE = "isActive";
+    /**
+     * The lookup key for a long that contains the timestamp (nanos) of the radio state change.
+     * {@hide}
+     */
+    public static final String EXTRA_REALTIME_NS = "tsNanos";
+
+    /**
+     * Broadcast Action: The setting for background data usage has changed
+     * values. Use {@link #getBackgroundDataSetting()} to get the current value.
+     * <p>
+     * If an application uses the network in the background, it should listen
+     * for this broadcast and stop using the background data if the value is
+     * {@code false}.
+     * <p>
+     *
+     * @deprecated As of {@link VERSION_CODES#ICE_CREAM_SANDWICH}, availability
+     *             of background data depends on several combined factors, and
+     *             this broadcast is no longer sent. Instead, when background
+     *             data is unavailable, {@link #getActiveNetworkInfo()} will now
+     *             appear disconnected. During first boot after a platform
+     *             upgrade, this broadcast will be sent once if
+     *             {@link #getBackgroundDataSetting()} was {@code false} before
+     *             the upgrade.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    @Deprecated
+    public static final String ACTION_BACKGROUND_DATA_SETTING_CHANGED =
+            "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED";
+
+    /**
+     * Broadcast Action: The network connection may not be good
+     * uses {@code ConnectivityManager.EXTRA_INET_CONDITION} and
+     * {@code ConnectivityManager.EXTRA_NETWORK_INFO} to specify
+     * the network and it's condition.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String INET_CONDITION_ACTION =
+            "android.net.conn.INET_CONDITION_ACTION";
+
+    /**
+     * Broadcast Action: A tetherable connection has come or gone.
+     * Uses {@code ConnectivityManager.EXTRA_AVAILABLE_TETHER},
+     * {@code ConnectivityManager.EXTRA_ACTIVE_TETHER} and
+     * {@code ConnectivityManager.EXTRA_ERRORED_TETHER} to indicate
+     * the current state of tethering.  Each include a list of
+     * interface names in that state (may be empty).
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_TETHER_STATE_CHANGED =
+            "android.net.conn.TETHER_STATE_CHANGED";
+
+    /**
+     * @hide
+     * gives a String[] listing all the interfaces configured for
+     * tethering and currently available for tethering.
+     */
+    public static final String EXTRA_AVAILABLE_TETHER = "availableArray";
+
+    /**
+     * @hide
+     * gives a String[] listing all the interfaces currently tethered
+     * (ie, has dhcp support and packets potentially forwarded/NATed)
+     */
+    public static final String EXTRA_ACTIVE_TETHER = "activeArray";
+
+    /**
+     * @hide
+     * gives a String[] listing all the interfaces we tried to tether and
+     * failed.  Use {@link #getLastTetherError} to find the error code
+     * for any interfaces listed here.
+     */
+    public static final String EXTRA_ERRORED_TETHER = "erroredArray";
+
+    /**
+     * Broadcast Action: The captive portal tracker has finished its test.
+     * Sent only while running Setup Wizard, in lieu of showing a user
+     * notification.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_CAPTIVE_PORTAL_TEST_COMPLETED =
+            "android.net.conn.CAPTIVE_PORTAL_TEST_COMPLETED";
+    /**
+     * The lookup key for a boolean that indicates whether a captive portal was detected.
+     * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+     * @hide
+     */
+    public static final String EXTRA_IS_CAPTIVE_PORTAL = "captivePortal";
+
+    /**
+     * The absence of a connection type.
+     * @hide
+     */
+    public static final int TYPE_NONE        = -1;
+
+    /**
+     * The Mobile data connection.  When active, all data traffic
+     * will use this network type's interface by default
+     * (it has a default route)
+     */
+    public static final int TYPE_MOBILE      = 0;
+    /**
+     * The WIFI data connection.  When active, all data traffic
+     * will use this network type's interface by default
+     * (it has a default route).
+     */
+    public static final int TYPE_WIFI        = 1;
+    /**
+     * An MMS-specific Mobile data connection.  This network type may use the
+     * same network interface as {@link #TYPE_MOBILE} or it may use a different
+     * one.  This is used by applications needing to talk to the carrier's
+     * Multimedia Messaging Service servers.
+     */
+    public static final int TYPE_MOBILE_MMS  = 2;
+    /**
+     * A SUPL-specific Mobile data connection.  This network type may use the
+     * same network interface as {@link #TYPE_MOBILE} or it may use a different
+     * one.  This is used by applications needing to talk to the carrier's
+     * Secure User Plane Location servers for help locating the device.
+     */
+    public static final int TYPE_MOBILE_SUPL = 3;
+    /**
+     * A DUN-specific Mobile data connection.  This network type may use the
+     * same network interface as {@link #TYPE_MOBILE} or it may use a different
+     * one.  This is sometimes by the system when setting up an upstream connection
+     * for tethering so that the carrier is aware of DUN traffic.
+     */
+    public static final int TYPE_MOBILE_DUN  = 4;
+    /**
+     * A High Priority Mobile data connection.  This network type uses the
+     * same network interface as {@link #TYPE_MOBILE} but the routing setup
+     * is different.  Only requesting processes will have access to the
+     * Mobile DNS servers and only IP's explicitly requested via {@link #requestRouteToHost}
+     * will route over this interface if no default route exists.
+     */
+    public static final int TYPE_MOBILE_HIPRI = 5;
+    /**
+     * The WiMAX data connection.  When active, all data traffic
+     * will use this network type's interface by default
+     * (it has a default route).
+     */
+    public static final int TYPE_WIMAX       = 6;
+
+    /**
+     * The Bluetooth data connection.  When active, all data traffic
+     * will use this network type's interface by default
+     * (it has a default route).
+     */
+    public static final int TYPE_BLUETOOTH   = 7;
+
+    /**
+     * Dummy data connection.  This should not be used on shipping devices.
+     */
+    public static final int TYPE_DUMMY       = 8;
+
+    /**
+     * The Ethernet data connection.  When active, all data traffic
+     * will use this network type's interface by default
+     * (it has a default route).
+     */
+    public static final int TYPE_ETHERNET    = 9;
+
+    /**
+     * Over the air Administration.
+     * {@hide}
+     */
+    public static final int TYPE_MOBILE_FOTA = 10;
+
+    /**
+     * IP Multimedia Subsystem.
+     * {@hide}
+     */
+    public static final int TYPE_MOBILE_IMS  = 11;
+
+    /**
+     * Carrier Branded Services.
+     * {@hide}
+     */
+    public static final int TYPE_MOBILE_CBS  = 12;
+
+    /**
+     * A Wi-Fi p2p connection. Only requesting processes will have access to
+     * the peers connected.
+     * {@hide}
+     */
+    public static final int TYPE_WIFI_P2P    = 13;
+
+    /**
+     * The network to use for initially attaching to the network
+     * {@hide}
+     */
+    public static final int TYPE_MOBILE_IA = 14;
+
+    /**
+     * The network that uses proxy to achieve connectivity.
+     * {@hide}
+     */
+    public static final int TYPE_PROXY = 16;
+
+    /** {@hide} */
+    public static final int MAX_RADIO_TYPE   = TYPE_PROXY;
+
+    /** {@hide} */
+    public static final int MAX_NETWORK_TYPE = TYPE_PROXY;
+
+    /**
+     * If you want to set the default network preference,you can directly
+     * change the networkAttributes array in framework's config.xml.
+     *
+     * @deprecated Since we support so many more networks now, the single
+     *             network default network preference can't really express
+     *             the hierarchy.  Instead, the default is defined by the
+     *             networkAttributes in config.xml.  You can determine
+     *             the current value by calling {@link #getNetworkPreference()}
+     *             from an App.
+     */
+    @Deprecated
+    public static final int DEFAULT_NETWORK_PREFERENCE = TYPE_WIFI;
+
+    /**
+     * Default value for {@link Settings.Global#CONNECTIVITY_CHANGE_DELAY} in
+     * milliseconds.  This was introduced because IPv6 routes seem to take a
+     * moment to settle - trying network activity before the routes are adjusted
+     * can lead to packets using the wrong interface or having the wrong IP address.
+     * This delay is a bit crude, but in the future hopefully we will have kernel
+     * notifications letting us know when it's safe to use the new network.
+     *
+     * @hide
+     */
+    public static final int CONNECTIVITY_CHANGE_DELAY_DEFAULT = 3000;
+
+    /**
+     * @hide
+     */
+    public final static int INVALID_NET_ID = 0;
+
+    /**
+     * @hide
+     */
+    public final static int REQUEST_ID_UNSET = 0;
+
+    private final IConnectivityManager mService;
+
+    private final String mPackageName;
+
+    private INetworkManagementService mNMService;
+
+    /**
+     * Tests if a given integer represents a valid network type.
+     * @param networkType the type to be tested
+     * @return a boolean.  {@code true} if the type is valid, else {@code false}
+     */
+    public static boolean isNetworkTypeValid(int networkType) {
+        return networkType >= 0 && networkType <= MAX_NETWORK_TYPE;
+    }
+
+    /**
+     * Returns a non-localized string representing a given network type.
+     * ONLY used for debugging output.
+     * @param type the type needing naming
+     * @return a String for the given type, or a string version of the type ("87")
+     * if no name is known.
+     * {@hide}
+     */
+    public static String getNetworkTypeName(int type) {
+        switch (type) {
+            case TYPE_MOBILE:
+                return "MOBILE";
+            case TYPE_WIFI:
+                return "WIFI";
+            case TYPE_MOBILE_MMS:
+                return "MOBILE_MMS";
+            case TYPE_MOBILE_SUPL:
+                return "MOBILE_SUPL";
+            case TYPE_MOBILE_DUN:
+                return "MOBILE_DUN";
+            case TYPE_MOBILE_HIPRI:
+                return "MOBILE_HIPRI";
+            case TYPE_WIMAX:
+                return "WIMAX";
+            case TYPE_BLUETOOTH:
+                return "BLUETOOTH";
+            case TYPE_DUMMY:
+                return "DUMMY";
+            case TYPE_ETHERNET:
+                return "ETHERNET";
+            case TYPE_MOBILE_FOTA:
+                return "MOBILE_FOTA";
+            case TYPE_MOBILE_IMS:
+                return "MOBILE_IMS";
+            case TYPE_MOBILE_CBS:
+                return "MOBILE_CBS";
+            case TYPE_WIFI_P2P:
+                return "WIFI_P2P";
+            case TYPE_MOBILE_IA:
+                return "MOBILE_IA";
+            case TYPE_PROXY:
+                return "PROXY";
+            default:
+                return Integer.toString(type);
+        }
+    }
+
+    /**
+     * Checks if a given type uses the cellular data connection.
+     * This should be replaced in the future by a network property.
+     * @param networkType the type to check
+     * @return a boolean - {@code true} if uses cellular network, else {@code false}
+     * {@hide}
+     */
+    public static boolean isNetworkTypeMobile(int networkType) {
+        switch (networkType) {
+            case TYPE_MOBILE:
+            case TYPE_MOBILE_MMS:
+            case TYPE_MOBILE_SUPL:
+            case TYPE_MOBILE_DUN:
+            case TYPE_MOBILE_HIPRI:
+            case TYPE_MOBILE_FOTA:
+            case TYPE_MOBILE_IMS:
+            case TYPE_MOBILE_CBS:
+            case TYPE_MOBILE_IA:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Checks if the given network type is backed by a Wi-Fi radio.
+     *
+     * @hide
+     */
+    public static boolean isNetworkTypeWifi(int networkType) {
+        switch (networkType) {
+            case TYPE_WIFI:
+            case TYPE_WIFI_P2P:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Checks if the given network type should be exempt from VPN routing rules
+     *
+     * @hide
+     */
+    public static boolean isNetworkTypeExempt(int networkType) {
+        switch (networkType) {
+            case TYPE_MOBILE_MMS:
+            case TYPE_MOBILE_SUPL:
+            case TYPE_MOBILE_HIPRI:
+            case TYPE_MOBILE_IA:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Specifies the preferred network type.  When the device has more
+     * than one type available the preferred network type will be used.
+     *
+     * @param preference the network type to prefer over all others.  It is
+     *         unspecified what happens to the old preferred network in the
+     *         overall ordering.
+     * @deprecated Functionality has been removed as it no longer makes sense,
+     *             with many more than two networks - we'd need an array to express
+     *             preference.  Instead we use dynamic network properties of
+     *             the networks to describe their precedence.
+     */
+    public void setNetworkPreference(int preference) {
+    }
+
+    /**
+     * Retrieves the current preferred network type.
+     *
+     * @return an integer representing the preferred network type
+     *
+     * <p>This method requires the caller to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * @deprecated Functionality has been removed as it no longer makes sense,
+     *             with many more than two networks - we'd need an array to express
+     *             preference.  Instead we use dynamic network properties of
+     *             the networks to describe their precedence.
+     */
+    public int getNetworkPreference() {
+        return TYPE_NONE;
+    }
+
+    /**
+     * Returns details about the currently active default data network. When
+     * connected, this network is the default route for outgoing connections.
+     * You should always check {@link NetworkInfo#isConnected()} before initiating
+     * network traffic. This may return {@code null} when there is no default
+     * network.
+     *
+     * @return a {@link NetworkInfo} object for the current default network
+     *        or {@code null} if no network default network is currently active
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     */
+    public NetworkInfo getActiveNetworkInfo() {
+        try {
+            return mService.getActiveNetworkInfo();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns details about the currently active default data network
+     * for a given uid.  This is for internal use only to avoid spying
+     * other apps.
+     *
+     * @return a {@link NetworkInfo} object for the current default network
+     *        for the given uid or {@code null} if no default network is
+     *        available for the specified uid.
+     *
+     * <p>This method requires the caller to hold the permission
+     * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}
+     * {@hide}
+     */
+    public NetworkInfo getActiveNetworkInfoForUid(int uid) {
+        try {
+            return mService.getActiveNetworkInfoForUid(uid);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns connection status information about a particular
+     * network type.
+     *
+     * @param networkType integer specifying which networkType in
+     *        which you're interested.
+     * @return a {@link NetworkInfo} object for the requested
+     *        network type or {@code null} if the type is not
+     *        supported by the device.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     */
+    public NetworkInfo getNetworkInfo(int networkType) {
+        try {
+            return mService.getNetworkInfo(networkType);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns connection status information about all network
+     * types supported by the device.
+     *
+     * @return an array of {@link NetworkInfo} objects.  Check each
+     * {@link NetworkInfo#getType} for which type each applies.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     */
+    public NetworkInfo[] getAllNetworkInfo() {
+        try {
+            return mService.getAllNetworkInfo();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns details about the Provisioning or currently active default data network. When
+     * connected, this network is the default route for outgoing connections.
+     * You should always check {@link NetworkInfo#isConnected()} before initiating
+     * network traffic. This may return {@code null} when there is no default
+     * network.
+     *
+     * @return a {@link NetworkInfo} object for the current default network
+     *        or {@code null} if no network default network is currently active
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     *
+     * {@hide}
+     */
+    public NetworkInfo getProvisioningOrActiveNetworkInfo() {
+        try {
+            return mService.getProvisioningOrActiveNetworkInfo();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the IP information for the current default network.
+     *
+     * @return a {@link LinkProperties} object describing the IP info
+     *        for the current default network, or {@code null} if there
+     *        is no current default network.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * {@hide}
+     */
+    public LinkProperties getActiveLinkProperties() {
+        try {
+            return mService.getActiveLinkProperties();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the IP information for a given network type.
+     *
+     * @param networkType the network type of interest.
+     * @return a {@link LinkProperties} object describing the IP info
+     *        for the given networkType, or {@code null} if there is
+     *        no current default network.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * {@hide}
+     */
+    public LinkProperties getLinkProperties(int networkType) {
+        try {
+            return mService.getLinkPropertiesForType(networkType);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Get the {@link LinkProperties} for the given {@link Network}.  This
+     * will return {@code null} if the network is unknown.
+     *
+     * @param network The {@link Network} object identifying the network in question.
+     * @return The {@link LinkProperties} for the network, or {@code null}.
+     **/
+    public LinkProperties getLinkProperties(Network network) {
+        try {
+            return mService.getLinkProperties(network);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Get the {@link NetworkCapabilities} for the given {@link Network}.  This
+     * will return {@code null} if the network is unknown.
+     *
+     * @param network The {@link Network} object identifying the network in question.
+     * @return The {@link NetworkCapabilities} for the network, or {@code null}.
+     */
+    public NetworkCapabilities getNetworkCapabilities(Network network) {
+        try {
+            return mService.getNetworkCapabilities(network);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Tells each network type to set its radio power state as directed.
+     *
+     * @param turnOn a boolean, {@code true} to turn the radios on,
+     *        {@code false} to turn them off.
+     * @return a boolean, {@code true} indicating success.  All network types
+     *        will be tried, even if some fail.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}.
+     * {@hide}
+     */
+// TODO - check for any callers and remove
+//    public boolean setRadios(boolean turnOn) {
+//        try {
+//            return mService.setRadios(turnOn);
+//        } catch (RemoteException e) {
+//            return false;
+//        }
+//    }
+
+    /**
+     * Tells a given networkType to set its radio power state as directed.
+     *
+     * @param networkType the int networkType of interest.
+     * @param turnOn a boolean, {@code true} to turn the radio on,
+     *        {@code} false to turn it off.
+     * @return a boolean, {@code true} indicating success.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}.
+     * {@hide}
+     */
+// TODO - check for any callers and remove
+//    public boolean setRadio(int networkType, boolean turnOn) {
+//        try {
+//            return mService.setRadio(networkType, turnOn);
+//        } catch (RemoteException e) {
+//            return false;
+//        }
+//    }
+
+    /**
+     * Tells the underlying networking system that the caller wants to
+     * begin using the named feature. The interpretation of {@code feature}
+     * is completely up to each networking implementation.
+     * <p>This method requires the caller to hold the permission
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}.
+     * @param networkType specifies which network the request pertains to
+     * @param feature the name of the feature to be used
+     * @return an integer value representing the outcome of the request.
+     * The interpretation of this value is specific to each networking
+     * implementation+feature combination, except that the value {@code -1}
+     * always indicates failure.
+     *
+     * @deprecated Deprecated in favor of the cleaner {@link #requestNetwork} api.
+     */
+    public int startUsingNetworkFeature(int networkType, String feature) {
+        NetworkCapabilities netCap = networkCapabilitiesForFeature(networkType, feature);
+        if (netCap == null) {
+            Log.d(TAG, "Can't satisfy startUsingNetworkFeature for " + networkType + ", " +
+                    feature);
+            return PhoneConstants.APN_REQUEST_FAILED;
+        }
+
+        NetworkRequest request = null;
+        synchronized (sLegacyRequests) {
+            LegacyRequest l = sLegacyRequests.get(netCap);
+            if (l != null) {
+                Log.d(TAG, "renewing startUsingNetworkFeature request " + l.networkRequest);
+                renewRequestLocked(l);
+                if (l.currentNetwork != null) {
+                    return PhoneConstants.APN_ALREADY_ACTIVE;
+                } else {
+                    return PhoneConstants.APN_REQUEST_STARTED;
+                }
+            }
+
+            request = requestNetworkForFeatureLocked(netCap);
+        }
+        if (request != null) {
+            Log.d(TAG, "starting startUsingNeworkFeature for request " + request);
+            return PhoneConstants.APN_REQUEST_STARTED;
+        } else {
+            Log.d(TAG, " request Failed");
+            return PhoneConstants.APN_REQUEST_FAILED;
+        }
+    }
+
+    /**
+     * Tells the underlying networking system that the caller is finished
+     * using the named feature. The interpretation of {@code feature}
+     * is completely up to each networking implementation.
+     * <p>This method requires the caller to hold the permission
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}.
+     * @param networkType specifies which network the request pertains to
+     * @param feature the name of the feature that is no longer needed
+     * @return an integer value representing the outcome of the request.
+     * The interpretation of this value is specific to each networking
+     * implementation+feature combination, except that the value {@code -1}
+     * always indicates failure.
+     *
+     * @deprecated Deprecated in favor of the cleaner {@link #requestNetwork} api.
+     */
+    public int stopUsingNetworkFeature(int networkType, String feature) {
+        NetworkCapabilities netCap = networkCapabilitiesForFeature(networkType, feature);
+        if (netCap == null) {
+            Log.d(TAG, "Can't satisfy stopUsingNetworkFeature for " + networkType + ", " +
+                    feature);
+            return -1;
+        }
+
+        NetworkRequest request = removeRequestForFeature(netCap);
+        if (request != null) {
+            Log.d(TAG, "stopUsingNetworkFeature for " + networkType + ", " + feature);
+            releaseNetworkRequest(request);
+        }
+        return 1;
+    }
+
+    /**
+     * Removes the NET_CAPABILITY_NOT_RESTRICTED capability from the given
+     * NetworkCapabilities object if all the capabilities it provides are
+     * typically provided by restricted networks.
+     *
+     * TODO: consider:
+     * - Moving to NetworkCapabilities
+     * - Renaming it to guessRestrictedCapability and make it set the
+     *   restricted capability bit in addition to clearing it.
+     * @hide
+     */
+    public static void maybeMarkCapabilitiesRestricted(NetworkCapabilities nc) {
+        for (int capability : nc.getCapabilities()) {
+            switch (capability) {
+                case NetworkCapabilities.NET_CAPABILITY_CBS:
+                case NetworkCapabilities.NET_CAPABILITY_DUN:
+                case NetworkCapabilities.NET_CAPABILITY_EIMS:
+                case NetworkCapabilities.NET_CAPABILITY_FOTA:
+                case NetworkCapabilities.NET_CAPABILITY_IA:
+                case NetworkCapabilities.NET_CAPABILITY_IMS:
+                case NetworkCapabilities.NET_CAPABILITY_RCS:
+                case NetworkCapabilities.NET_CAPABILITY_XCAP:
+                case NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED: //there by default
+                    continue;
+                default:
+                    // At least one capability usually provided by unrestricted
+                    // networks. Conclude that this network is unrestricted.
+                    return;
+            }
+        }
+        // All the capabilities are typically provided by restricted networks.
+        // Conclude that this network is restricted.
+        nc.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+    }
+
+    private NetworkCapabilities networkCapabilitiesForFeature(int networkType, String feature) {
+        if (networkType == TYPE_MOBILE) {
+            int cap = -1;
+            if ("enableMMS".equals(feature)) {
+                cap = NetworkCapabilities.NET_CAPABILITY_MMS;
+            } else if ("enableSUPL".equals(feature)) {
+                cap = NetworkCapabilities.NET_CAPABILITY_SUPL;
+            } else if ("enableDUN".equals(feature) || "enableDUNAlways".equals(feature)) {
+                cap = NetworkCapabilities.NET_CAPABILITY_DUN;
+            } else if ("enableHIPRI".equals(feature)) {
+                cap = NetworkCapabilities.NET_CAPABILITY_INTERNET;
+            } else if ("enableFOTA".equals(feature)) {
+                cap = NetworkCapabilities.NET_CAPABILITY_FOTA;
+            } else if ("enableIMS".equals(feature)) {
+                cap = NetworkCapabilities.NET_CAPABILITY_IMS;
+            } else if ("enableCBS".equals(feature)) {
+                cap = NetworkCapabilities.NET_CAPABILITY_CBS;
+            } else {
+                return null;
+            }
+            NetworkCapabilities netCap = new NetworkCapabilities();
+            netCap.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR).addCapability(cap);
+            maybeMarkCapabilitiesRestricted(netCap);
+            return netCap;
+        } else if (networkType == TYPE_WIFI) {
+            if ("p2p".equals(feature)) {
+                NetworkCapabilities netCap = new NetworkCapabilities();
+                netCap.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
+                netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_WIFI_P2P);
+                maybeMarkCapabilitiesRestricted(netCap);
+                return netCap;
+            }
+        }
+        return null;
+    }
+
+    private int legacyTypeForNetworkCapabilities(NetworkCapabilities netCap) {
+        if (netCap == null) return TYPE_NONE;
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
+            return TYPE_MOBILE_CBS;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_IMS)) {
+            return TYPE_MOBILE_IMS;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOTA)) {
+            return TYPE_MOBILE_FOTA;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN)) {
+            return TYPE_MOBILE_DUN;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL)) {
+            return TYPE_MOBILE_SUPL;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_MMS)) {
+            return TYPE_MOBILE_MMS;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+            return TYPE_MOBILE_HIPRI;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_WIFI_P2P)) {
+            return TYPE_WIFI_P2P;
+        }
+        return TYPE_NONE;
+    }
+
+    private static class LegacyRequest {
+        NetworkCapabilities networkCapabilities;
+        NetworkRequest networkRequest;
+        int expireSequenceNumber;
+        Network currentNetwork;
+        int delay = -1;
+        NetworkCallbackListener networkCallbackListener = new NetworkCallbackListener() {
+            @Override
+            public void onAvailable(NetworkRequest request, Network network) {
+                currentNetwork = network;
+                Log.d(TAG, "startUsingNetworkFeature got Network:" + network);
+                setProcessDefaultNetworkForHostResolution(network);
+            }
+            @Override
+            public void onLost(NetworkRequest request, Network network) {
+                if (network.equals(currentNetwork)) {
+                    currentNetwork = null;
+                    setProcessDefaultNetworkForHostResolution(null);
+                }
+                Log.d(TAG, "startUsingNetworkFeature lost Network:" + network);
+            }
+        };
+    }
+
+    private HashMap<NetworkCapabilities, LegacyRequest> sLegacyRequests =
+            new HashMap<NetworkCapabilities, LegacyRequest>();
+
+    private NetworkRequest findRequestForFeature(NetworkCapabilities netCap) {
+        synchronized (sLegacyRequests) {
+            LegacyRequest l = sLegacyRequests.get(netCap);
+            if (l != null) return l.networkRequest;
+        }
+        return null;
+    }
+
+    private void renewRequestLocked(LegacyRequest l) {
+        l.expireSequenceNumber++;
+        Log.d(TAG, "renewing request to seqNum " + l.expireSequenceNumber);
+        sendExpireMsgForFeature(l.networkCapabilities, l.expireSequenceNumber, l.delay);
+    }
+
+    private void expireRequest(NetworkCapabilities netCap, int sequenceNum) {
+        int ourSeqNum = -1;
+        synchronized (sLegacyRequests) {
+            LegacyRequest l = sLegacyRequests.get(netCap);
+            if (l == null) return;
+            ourSeqNum = l.expireSequenceNumber;
+            if (l.expireSequenceNumber == sequenceNum) {
+                releaseNetworkRequest(l.networkRequest);
+                sLegacyRequests.remove(netCap);
+            }
+        }
+        Log.d(TAG, "expireRequest with " + ourSeqNum + ", " + sequenceNum);
+    }
+
+    private NetworkRequest requestNetworkForFeatureLocked(NetworkCapabilities netCap) {
+        int delay = -1;
+        int type = legacyTypeForNetworkCapabilities(netCap);
+        try {
+            delay = mService.getRestoreDefaultNetworkDelay(type);
+        } catch (RemoteException e) {}
+        LegacyRequest l = new LegacyRequest();
+        l.networkCapabilities = netCap;
+        l.delay = delay;
+        l.expireSequenceNumber = 0;
+        l.networkRequest = sendRequestForNetwork(netCap, l.networkCallbackListener, 0,
+                REQUEST, type);
+        if (l.networkRequest == null) return null;
+        sLegacyRequests.put(netCap, l);
+        sendExpireMsgForFeature(netCap, l.expireSequenceNumber, delay);
+        return l.networkRequest;
+    }
+
+    private void sendExpireMsgForFeature(NetworkCapabilities netCap, int seqNum, int delay) {
+        if (delay >= 0) {
+            Log.d(TAG, "sending expire msg with seqNum " + seqNum + " and delay " + delay);
+            Message msg = sCallbackHandler.obtainMessage(EXPIRE_LEGACY_REQUEST, seqNum, 0, netCap);
+            sCallbackHandler.sendMessageDelayed(msg, delay);
+        }
+    }
+
+    private NetworkRequest removeRequestForFeature(NetworkCapabilities netCap) {
+        synchronized (sLegacyRequests) {
+            LegacyRequest l = sLegacyRequests.remove(netCap);
+            if (l == null) return null;
+            return l.networkRequest;
+        }
+    }
+
+    /**
+     * Ensure that a network route exists to deliver traffic to the specified
+     * host via the specified network interface. An attempt to add a route that
+     * already exists is ignored, but treated as successful.
+     * <p>This method requires the caller to hold the permission
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}.
+     * @param networkType the type of the network over which traffic to the specified
+     * host is to be routed
+     * @param hostAddress the IP address of the host to which the route is desired
+     * @return {@code true} on success, {@code false} on failure
+     *
+     * @deprecated Deprecated in favor of the {@link #requestNetwork},
+     *             {@link #setProcessDefaultNetwork} and {@link Network#getSocketFactory} api.
+     */
+    public boolean requestRouteToHost(int networkType, int hostAddress) {
+        InetAddress inetAddress = NetworkUtils.intToInetAddress(hostAddress);
+
+        if (inetAddress == null) {
+            return false;
+        }
+
+        return requestRouteToHostAddress(networkType, inetAddress);
+    }
+
+    /**
+     * Ensure that a network route exists to deliver traffic to the specified
+     * host via the specified network interface. An attempt to add a route that
+     * already exists is ignored, but treated as successful.
+     * <p>This method requires the caller to hold the permission
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}.
+     * @param networkType the type of the network over which traffic to the specified
+     * host is to be routed
+     * @param hostAddress the IP address of the host to which the route is desired
+     * @return {@code true} on success, {@code false} on failure
+     * @hide
+     * @deprecated Deprecated in favor of the {@link #requestNetwork} and
+     *             {@link #setProcessDefaultNetwork} api.
+     */
+    public boolean requestRouteToHostAddress(int networkType, InetAddress hostAddress) {
+        byte[] address = hostAddress.getAddress();
+        try {
+            return mService.requestRouteToHostAddress(networkType, address, mPackageName);
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the value of the setting for background data usage. If false,
+     * applications should not use the network if the application is not in the
+     * foreground. Developers should respect this setting, and check the value
+     * of this before performing any background data operations.
+     * <p>
+     * All applications that have background services that use the network
+     * should listen to {@link #ACTION_BACKGROUND_DATA_SETTING_CHANGED}.
+     * <p>
+     * @deprecated As of {@link VERSION_CODES#ICE_CREAM_SANDWICH}, availability of
+     * background data depends on several combined factors, and this method will
+     * always return {@code true}. Instead, when background data is unavailable,
+     * {@link #getActiveNetworkInfo()} will now appear disconnected.
+     *
+     * @return Whether background data usage is allowed.
+     */
+    @Deprecated
+    public boolean getBackgroundDataSetting() {
+        // assume that background data is allowed; final authority is
+        // NetworkInfo which may be blocked.
+        return true;
+    }
+
+    /**
+     * Sets the value of the setting for background data usage.
+     *
+     * @param allowBackgroundData Whether an application should use data while
+     *            it is in the background.
+     *
+     * @attr ref android.Manifest.permission#CHANGE_BACKGROUND_DATA_SETTING
+     * @see #getBackgroundDataSetting()
+     * @hide
+     */
+    @Deprecated
+    public void setBackgroundDataSetting(boolean allowBackgroundData) {
+        // ignored
+    }
+
+    /**
+     * Return quota status for the current active network, or {@code null} if no
+     * network is active. Quota status can change rapidly, so these values
+     * shouldn't be cached.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     *
+     * @hide
+     */
+    public NetworkQuotaInfo getActiveNetworkQuotaInfo() {
+        try {
+            return mService.getActiveNetworkQuotaInfo();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * @hide
+     * @deprecated Talk to TelephonyManager directly
+     */
+    public boolean getMobileDataEnabled() {
+        IBinder b = ServiceManager.getService(Context.TELEPHONY_SERVICE);
+        if (b != null) {
+            try {
+                ITelephony it = ITelephony.Stub.asInterface(b);
+                return it.getDataEnabled();
+            } catch (RemoteException e) { }
+        }
+        return false;
+    }
+
+    /**
+     * Callback for use with {@link ConnectivityManager#registerNetworkActiveListener} to
+     * find out when the current network has gone in to a high power state.
+     */
+    public interface OnNetworkActiveListener {
+        /**
+         * Called on the main thread of the process to report that the current data network
+         * has become active, and it is now a good time to perform any pending network
+         * operations.  Note that this listener only tells you when the network becomes
+         * active; if at any other time you want to know whether it is active (and thus okay
+         * to initiate network traffic), you can retrieve its instantaneous state with
+         * {@link ConnectivityManager#isNetworkActive}.
+         */
+        public void onNetworkActive();
+    }
+
+    private INetworkManagementService getNetworkManagementService() {
+        synchronized (this) {
+            if (mNMService != null) {
+                return mNMService;
+            }
+            IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE);
+            mNMService = INetworkManagementService.Stub.asInterface(b);
+            return mNMService;
+        }
+    }
+
+    private final ArrayMap<OnNetworkActiveListener, INetworkActivityListener>
+            mNetworkActivityListeners
+                    = new ArrayMap<OnNetworkActiveListener, INetworkActivityListener>();
+
+    /**
+     * Start listening to reports when the data network is active, meaning it is
+     * a good time to perform network traffic.  Use {@link #isNetworkActive()}
+     * to determine the current state of the network after registering the listener.
+     *
+     * @param l The listener to be told when the network is active.
+     */
+    public void registerNetworkActiveListener(final OnNetworkActiveListener l) {
+        INetworkActivityListener rl = new INetworkActivityListener.Stub() {
+            @Override
+            public void onNetworkActive() throws RemoteException {
+                l.onNetworkActive();
+            }
+        };
+
+        try {
+            getNetworkManagementService().registerNetworkActivityListener(rl);
+            mNetworkActivityListeners.put(l, rl);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Remove network active listener previously registered with
+     * {@link #registerNetworkActiveListener}.
+     *
+     * @param l Previously registered listener.
+     */
+    public void unregisterNetworkActiveListener(OnNetworkActiveListener l) {
+        INetworkActivityListener rl = mNetworkActivityListeners.get(l);
+        if (rl == null) {
+            throw new IllegalArgumentException("Listener not registered: " + l);
+        }
+        try {
+            getNetworkManagementService().unregisterNetworkActivityListener(rl);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Return whether the data network is currently active.  An active network means that
+     * it is currently in a high power state for performing data transmission.  On some
+     * types of networks, it may be expensive to move and stay in such a state, so it is
+     * more power efficient to batch network traffic together when the radio is already in
+     * this state.  This method tells you whether right now is currently a good time to
+     * initiate network traffic, as the network is already active.
+     */
+    public boolean isNetworkActive() {
+        try {
+            return getNetworkManagementService().isNetworkActive();
+        } catch (RemoteException e) {
+        }
+        return false;
+    }
+
+    /**
+     * {@hide}
+     */
+    public ConnectivityManager(IConnectivityManager service, String packageName) {
+        mService = checkNotNull(service, "missing IConnectivityManager");
+        mPackageName = checkNotNull(packageName, "missing package name");
+    }
+
+    /** {@hide} */
+    public static ConnectivityManager from(Context context) {
+        return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+    }
+
+    /**
+     * Get the set of tetherable, available interfaces.  This list is limited by
+     * device configuration and current interface existence.
+     *
+     * @return an array of 0 or more Strings of tetherable interface names.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * {@hide}
+     */
+    public String[] getTetherableIfaces() {
+        try {
+            return mService.getTetherableIfaces();
+        } catch (RemoteException e) {
+            return new String[0];
+        }
+    }
+
+    /**
+     * Get the set of tethered interfaces.
+     *
+     * @return an array of 0 or more String of currently tethered interface names.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * {@hide}
+     */
+    public String[] getTetheredIfaces() {
+        try {
+            return mService.getTetheredIfaces();
+        } catch (RemoteException e) {
+            return new String[0];
+        }
+    }
+
+    /**
+     * Get the set of interface names which attempted to tether but
+     * failed.  Re-attempting to tether may cause them to reset to the Tethered
+     * state.  Alternatively, causing the interface to be destroyed and recreated
+     * may cause them to reset to the available state.
+     * {@link ConnectivityManager#getLastTetherError} can be used to get more
+     * information on the cause of the errors.
+     *
+     * @return an array of 0 or more String indicating the interface names
+     *        which failed to tether.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * {@hide}
+     */
+    public String[] getTetheringErroredIfaces() {
+        try {
+            return mService.getTetheringErroredIfaces();
+        } catch (RemoteException e) {
+            return new String[0];
+        }
+    }
+
+    /**
+     * Attempt to tether the named interface.  This will setup a dhcp server
+     * on the interface, forward and NAT IP packets and forward DNS requests
+     * to the best active upstream network interface.  Note that if no upstream
+     * IP network interface is available, dhcp will still run and traffic will be
+     * allowed between the tethered devices and this device, though upstream net
+     * access will of course fail until an upstream network interface becomes
+     * active.
+     *
+     * @param iface the interface name to tether.
+     * @return error a {@code TETHER_ERROR} value indicating success or failure type
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}.
+     * {@hide}
+     */
+    public int tether(String iface) {
+        try {
+            return mService.tether(iface);
+        } catch (RemoteException e) {
+            return TETHER_ERROR_SERVICE_UNAVAIL;
+        }
+    }
+
+    /**
+     * Stop tethering the named interface.
+     *
+     * @param iface the interface name to untether.
+     * @return error a {@code TETHER_ERROR} value indicating success or failure type
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}.
+     * {@hide}
+     */
+    public int untether(String iface) {
+        try {
+            return mService.untether(iface);
+        } catch (RemoteException e) {
+            return TETHER_ERROR_SERVICE_UNAVAIL;
+        }
+    }
+
+    /**
+     * Check if the device allows for tethering.  It may be disabled via
+     * {@code ro.tether.denied} system property, Settings.TETHER_SUPPORTED or
+     * due to device configuration.
+     *
+     * @return a boolean - {@code true} indicating Tethering is supported.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * {@hide}
+     */
+    public boolean isTetheringSupported() {
+        try {
+            return mService.isTetheringSupported();
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Get the list of regular expressions that define any tetherable
+     * USB network interfaces.  If USB tethering is not supported by the
+     * device, this list should be empty.
+     *
+     * @return an array of 0 or more regular expression Strings defining
+     *        what interfaces are considered tetherable usb interfaces.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * {@hide}
+     */
+    public String[] getTetherableUsbRegexs() {
+        try {
+            return mService.getTetherableUsbRegexs();
+        } catch (RemoteException e) {
+            return new String[0];
+        }
+    }
+
+    /**
+     * Get the list of regular expressions that define any tetherable
+     * Wifi network interfaces.  If Wifi tethering is not supported by the
+     * device, this list should be empty.
+     *
+     * @return an array of 0 or more regular expression Strings defining
+     *        what interfaces are considered tetherable wifi interfaces.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * {@hide}
+     */
+    public String[] getTetherableWifiRegexs() {
+        try {
+            return mService.getTetherableWifiRegexs();
+        } catch (RemoteException e) {
+            return new String[0];
+        }
+    }
+
+    /**
+     * Get the list of regular expressions that define any tetherable
+     * Bluetooth network interfaces.  If Bluetooth tethering is not supported by the
+     * device, this list should be empty.
+     *
+     * @return an array of 0 or more regular expression Strings defining
+     *        what interfaces are considered tetherable bluetooth interfaces.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * {@hide}
+     */
+    public String[] getTetherableBluetoothRegexs() {
+        try {
+            return mService.getTetherableBluetoothRegexs();
+        } catch (RemoteException e) {
+            return new String[0];
+        }
+    }
+
+    /**
+     * Attempt to both alter the mode of USB and Tethering of USB.  A
+     * utility method to deal with some of the complexity of USB - will
+     * attempt to switch to Rndis and subsequently tether the resulting
+     * interface on {@code true} or turn off tethering and switch off
+     * Rndis on {@code false}.
+     *
+     * @param enable a boolean - {@code true} to enable tethering
+     * @return error a {@code TETHER_ERROR} value indicating success or failure type
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}.
+     * {@hide}
+     */
+    public int setUsbTethering(boolean enable) {
+        try {
+            return mService.setUsbTethering(enable);
+        } catch (RemoteException e) {
+            return TETHER_ERROR_SERVICE_UNAVAIL;
+        }
+    }
+
+    /** {@hide} */
+    public static final int TETHER_ERROR_NO_ERROR           = 0;
+    /** {@hide} */
+    public static final int TETHER_ERROR_UNKNOWN_IFACE      = 1;
+    /** {@hide} */
+    public static final int TETHER_ERROR_SERVICE_UNAVAIL    = 2;
+    /** {@hide} */
+    public static final int TETHER_ERROR_UNSUPPORTED        = 3;
+    /** {@hide} */
+    public static final int TETHER_ERROR_UNAVAIL_IFACE      = 4;
+    /** {@hide} */
+    public static final int TETHER_ERROR_MASTER_ERROR       = 5;
+    /** {@hide} */
+    public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6;
+    /** {@hide} */
+    public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7;
+    /** {@hide} */
+    public static final int TETHER_ERROR_ENABLE_NAT_ERROR     = 8;
+    /** {@hide} */
+    public static final int TETHER_ERROR_DISABLE_NAT_ERROR    = 9;
+    /** {@hide} */
+    public static final int TETHER_ERROR_IFACE_CFG_ERROR      = 10;
+
+    /**
+     * Get a more detailed error code after a Tethering or Untethering
+     * request asynchronously failed.
+     *
+     * @param iface The name of the interface of interest
+     * @return error The error code of the last error tethering or untethering the named
+     *               interface
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * {@hide}
+     */
+    public int getLastTetherError(String iface) {
+        try {
+            return mService.getLastTetherError(iface);
+        } catch (RemoteException e) {
+            return TETHER_ERROR_SERVICE_UNAVAIL;
+        }
+    }
+
+    /**
+     * Try to ensure the device stays awake until we connect with the next network.
+     * Actually just holds a wakelock for a number of seconds while we try to connect
+     * to any default networks.  This will expire if the timeout passes or if we connect
+     * to a default after this is called.  For internal use only.
+     *
+     * @param forWhom the name of the network going down for logging purposes
+     * @return {@code true} on success, {@code false} on failure
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}.
+     * {@hide}
+     */
+    public boolean requestNetworkTransitionWakelock(String forWhom) {
+        try {
+            mService.requestNetworkTransitionWakelock(forWhom);
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Report network connectivity status.  This is currently used only
+     * to alter status bar UI.
+     *
+     * @param networkType The type of network you want to report on
+     * @param percentage The quality of the connection 0 is bad, 100 is good
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#STATUS_BAR}.
+     * {@hide}
+     */
+    public void reportInetCondition(int networkType, int percentage) {
+        try {
+            mService.reportInetCondition(networkType, percentage);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Report a problem network to the framework.  This provides a hint to the system
+     * that there might be connectivity problems on this network and may cause 
+     * the framework to re-evaluate network connectivity and/or switch to another
+     * network.
+     *
+     * @param network The {@link Network} the application was attempting to use
+     *                or {@code null} to indicate the current default network.
+     */
+    public void reportBadNetwork(Network network) {
+        try {
+            mService.reportBadNetwork(network);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Set a network-independent global http proxy.  This is not normally what you want
+     * for typical HTTP proxies - they are general network dependent.  However if you're
+     * doing something unusual like general internal filtering this may be useful.  On
+     * a private network where the proxy is not accessible, you may break HTTP using this.
+     *
+     * @param p The a {@link ProxyInfo} object defining the new global
+     *        HTTP proxy.  A {@code null} value will clear the global HTTP proxy.
+     *
+     * <p>This method requires the call to hold the permission
+     * android.Manifest.permission#CONNECTIVITY_INTERNAL.
+     * @hide
+     */
+    public void setGlobalProxy(ProxyInfo p) {
+        try {
+            mService.setGlobalProxy(p);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Retrieve any network-independent global HTTP proxy.
+     *
+     * @return {@link ProxyInfo} for the current global HTTP proxy or {@code null}
+     *        if no global HTTP proxy is set.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * @hide
+     */
+    public ProxyInfo getGlobalProxy() {
+        try {
+            return mService.getGlobalProxy();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Get the HTTP proxy settings for the current default network.  Note that
+     * if a global proxy is set, it will override any per-network setting.
+     *
+     * @return the {@link ProxyInfo} for the current HTTP proxy, or {@code null} if no
+     *        HTTP proxy is active.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * {@hide}
+     * @deprecated Deprecated in favor of {@link #getLinkProperties}
+     */
+    public ProxyInfo getProxy() {
+        try {
+            return mService.getProxy();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Sets a secondary requirement bit for the given networkType.
+     * This requirement bit is generally under the control of the carrier
+     * or its agents and is not directly controlled by the user.
+     *
+     * @param networkType The network who's dependence has changed
+     * @param met Boolean - true if network use is OK, false if not
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}.
+     * {@hide}
+     */
+    public void setDataDependency(int networkType, boolean met) {
+        try {
+            mService.setDataDependency(networkType, met);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Returns true if the hardware supports the given network type
+     * else it returns false.  This doesn't indicate we have coverage
+     * or are authorized onto a network, just whether or not the
+     * hardware supports it.  For example a GSM phone without a SIM
+     * should still return {@code true} for mobile data, but a wifi only
+     * tablet would return {@code false}.
+     *
+     * @param networkType The network type we'd like to check
+     * @return {@code true} if supported, else {@code false}
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     * @hide
+     */
+    public boolean isNetworkSupported(int networkType) {
+        try {
+            return mService.isNetworkSupported(networkType);
+        } catch (RemoteException e) {}
+        return false;
+    }
+
+    /**
+     * Returns if the currently active data network is metered. A network is
+     * classified as metered when the user is sensitive to heavy data usage on
+     * that connection due to monetary costs, data limitations or
+     * battery/performance issues. You should check this before doing large
+     * data transfers, and warn the user or delay the operation until another
+     * network is available.
+     *
+     * @return {@code true} if large transfers should be avoided, otherwise
+     *        {@code false}.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+     */
+    public boolean isActiveNetworkMetered() {
+        try {
+            return mService.isActiveNetworkMetered();
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    /**
+     * If the LockdownVpn mechanism is enabled, updates the vpn
+     * with a reload of its profile.
+     *
+     * @return a boolean with {@code} indicating success
+     *
+     * <p>This method can only be called by the system UID
+     * {@hide}
+     */
+    public boolean updateLockdownVpn() {
+        try {
+            return mService.updateLockdownVpn();
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Signal that the captive portal check on the indicated network
+     * is complete and whether its a captive portal or not.
+     *
+     * @param info the {@link NetworkInfo} object for the networkType
+     *        in question.
+     * @param isCaptivePortal true/false.
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}.
+     * {@hide}
+     */
+    public void captivePortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) {
+        try {
+            mService.captivePortalCheckCompleted(info, isCaptivePortal);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Supply the backend messenger for a network tracker
+     *
+     * @param networkType NetworkType to set
+     * @param messenger {@link Messenger}
+     * {@hide}
+     */
+    public void supplyMessenger(int networkType, Messenger messenger) {
+        try {
+            mService.supplyMessenger(networkType, messenger);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Check mobile provisioning.
+     *
+     * @param suggestedTimeOutMs, timeout in milliseconds
+     *
+     * @return time out that will be used, maybe less that suggestedTimeOutMs
+     * -1 if an error.
+     *
+     * {@hide}
+     */
+    public int checkMobileProvisioning(int suggestedTimeOutMs) {
+        int timeOutMs = -1;
+        try {
+            timeOutMs = mService.checkMobileProvisioning(suggestedTimeOutMs);
+        } catch (RemoteException e) {
+        }
+        return timeOutMs;
+    }
+
+    /**
+     * Get the mobile provisioning url.
+     * {@hide}
+     */
+    public String getMobileProvisioningUrl() {
+        try {
+            return mService.getMobileProvisioningUrl();
+        } catch (RemoteException e) {
+        }
+        return null;
+    }
+
+    /**
+     * Get the mobile redirected provisioning url.
+     * {@hide}
+     */
+    public String getMobileRedirectedProvisioningUrl() {
+        try {
+            return mService.getMobileRedirectedProvisioningUrl();
+        } catch (RemoteException e) {
+        }
+        return null;
+    }
+
+    /**
+     * get the information about a specific network link
+     * @hide
+     */
+    public LinkQualityInfo getLinkQualityInfo(int networkType) {
+        try {
+            LinkQualityInfo li = mService.getLinkQualityInfo(networkType);
+            return li;
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * get the information of currently active network link
+     * @hide
+     */
+    public LinkQualityInfo getActiveLinkQualityInfo() {
+        try {
+            LinkQualityInfo li = mService.getActiveLinkQualityInfo();
+            return li;
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * get the information of all network links
+     * @hide
+     */
+    public LinkQualityInfo[] getAllLinkQualityInfo() {
+        try {
+            LinkQualityInfo[] li = mService.getAllLinkQualityInfo();
+            return li;
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Set sign in error notification to visible or in visible
+     *
+     * @param visible
+     * @param networkType
+     *
+     * {@hide}
+     */
+    public void setProvisioningNotificationVisible(boolean visible, int networkType,
+            String extraInfo, String url) {
+        try {
+            mService.setProvisioningNotificationVisible(visible, networkType, extraInfo, url);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Set the value for enabling/disabling airplane mode
+     *
+     * @param enable whether to enable airplane mode or not
+     *
+     * <p>This method requires the call to hold the permission
+     * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}.
+     * @hide
+     */
+    public void setAirplaneMode(boolean enable) {
+        try {
+            mService.setAirplaneMode(enable);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /** {@hide} */
+    public void registerNetworkFactory(Messenger messenger, String name) {
+        try {
+            mService.registerNetworkFactory(messenger, name);
+        } catch (RemoteException e) { }
+    }
+
+    /** {@hide} */
+    public void unregisterNetworkFactory(Messenger messenger) {
+        try {
+            mService.unregisterNetworkFactory(messenger);
+        } catch (RemoteException e) { }
+    }
+
+    /** {@hide} */
+    public void registerNetworkAgent(Messenger messenger, NetworkInfo ni, LinkProperties lp,
+            NetworkCapabilities nc, int score) {
+        try {
+            mService.registerNetworkAgent(messenger, ni, lp, nc, score);
+        } catch (RemoteException e) { }
+    }
+
+    /**
+     * Base class for NetworkRequest callbacks.  Used for notifications about network
+     * changes.  Should be extended by applications wanting notifications.
+     */
+    public static class NetworkCallbackListener {
+        /** @hide */
+        public static final int PRECHECK     = 1;
+        /** @hide */
+        public static final int AVAILABLE    = 2;
+        /** @hide */
+        public static final int LOSING       = 3;
+        /** @hide */
+        public static final int LOST         = 4;
+        /** @hide */
+        public static final int UNAVAIL      = 5;
+        /** @hide */
+        public static final int CAP_CHANGED  = 6;
+        /** @hide */
+        public static final int PROP_CHANGED = 7;
+        /** @hide */
+        public static final int CANCELED     = 8;
+
+        /**
+         * @hide
+         * Called whenever the framework connects to a network that it may use to
+         * satisfy this request
+         */
+        public void onPreCheck(NetworkRequest networkRequest, Network network) {}
+
+        /**
+         * Called when the framework connects and has declared new network ready for use.
+         *
+         * @param networkRequest The {@link NetworkRequest} used to initiate the request.
+         * @param network The {@link Network} of the satisfying network.
+         */
+        public void onAvailable(NetworkRequest networkRequest, Network network) {}
+
+        /**
+         * Called when the network is about to be disconnected.  Often paired with an
+         * {@link NetworkCallbackListener#onAvailable} call with the new replacement network
+         * for graceful handover.  This may not be called if we have a hard loss
+         * (loss without warning).  This may be followed by either a
+         * {@link NetworkCallbackListener#onLost} call or a
+         * {@link NetworkCallbackListener#onAvailable} call for this network depending
+         * on whether we lose or regain it.
+         *
+         * @param networkRequest The {@link NetworkRequest} used to initiate the request.
+         * @param network The {@link Network} of the failing network.
+         * @param maxSecToLive The time in seconds the framework will attempt to keep the
+         *                     network connected.  Note that the network may suffers a
+         *                     hard loss at any time.
+         */
+        public void onLosing(NetworkRequest networkRequest, Network network, int maxSecToLive) {}
+
+        /**
+         * Called when the framework has a hard loss of the network or when the
+         * graceful failure ends.
+         *
+         * @param networkRequest The {@link NetworkRequest} used to initiate the request.
+         * @param network The {@link Network} lost.
+         */
+        public void onLost(NetworkRequest networkRequest, Network network) {}
+
+        /**
+         * Called if no network is found in the given timeout time.  If no timeout is given,
+         * this will not be called.
+         * @hide
+         */
+        public void onUnavailable(NetworkRequest networkRequest) {}
+
+        /**
+         * Called when the network the framework connected to for this request
+         * changes capabilities but still satisfies the stated need.
+         *
+         * @param networkRequest The {@link NetworkRequest} used to initiate the request.
+         * @param network The {@link Network} whose capabilities have changed.
+         * @param networkCapabilities The new {@link NetworkCapabilities} for this network.
+         */
+        public void onNetworkCapabilitiesChanged(NetworkRequest networkRequest, Network network,
+                NetworkCapabilities networkCapabilities) {}
+
+        /**
+         * Called when the network the framework connected to for this request
+         * changes {@link LinkProperties}.
+         *
+         * @param networkRequest The {@link NetworkRequest} used to initiate the request.
+         * @param network The {@link Network} whose link properties have changed.
+         * @param linkProperties The new {@link LinkProperties} for this network.
+         */
+        public void onLinkPropertiesChanged(NetworkRequest networkRequest, Network network,
+                LinkProperties linkProperties) {}
+
+        /**
+         * Called when a {@link #releaseNetworkRequest} call concludes and the registered
+         * callbacks will no longer be used.
+         *
+         * @param networkRequest The {@link NetworkRequest} used to initiate the request.
+         */
+        public void onReleased(NetworkRequest networkRequest) {}
+    }
+
+    private static final int BASE = Protocol.BASE_CONNECTIVITY_MANAGER;
+    /** @hide obj = pair(NetworkRequest, Network) */
+    public static final int CALLBACK_PRECHECK           = BASE + 1;
+    /** @hide obj = pair(NetworkRequest, Network) */
+    public static final int CALLBACK_AVAILABLE          = BASE + 2;
+    /** @hide obj = pair(NetworkRequest, Network), arg1 = ttl */
+    public static final int CALLBACK_LOSING             = BASE + 3;
+    /** @hide obj = pair(NetworkRequest, Network) */
+    public static final int CALLBACK_LOST               = BASE + 4;
+    /** @hide obj = NetworkRequest */
+    public static final int CALLBACK_UNAVAIL            = BASE + 5;
+    /** @hide obj = pair(NetworkRequest, Network) */
+    public static final int CALLBACK_CAP_CHANGED        = BASE + 6;
+    /** @hide obj = pair(NetworkRequest, Network) */
+    public static final int CALLBACK_IP_CHANGED         = BASE + 7;
+    /** @hide obj = NetworkRequest */
+    public static final int CALLBACK_RELEASED           = BASE + 8;
+    /** @hide */
+    public static final int CALLBACK_EXIT               = BASE + 9;
+    /** @hide obj = NetworkCapabilities, arg1 = seq number */
+    private static final int EXPIRE_LEGACY_REQUEST      = BASE + 10;
+
+    private class CallbackHandler extends Handler {
+        private final HashMap<NetworkRequest, NetworkCallbackListener>mCallbackMap;
+        private final AtomicInteger mRefCount;
+        private static final String TAG = "ConnectivityManager.CallbackHandler";
+        private final ConnectivityManager mCm;
+
+        CallbackHandler(Looper looper, HashMap<NetworkRequest, NetworkCallbackListener>callbackMap,
+                AtomicInteger refCount, ConnectivityManager cm) {
+            super(looper);
+            mCallbackMap = callbackMap;
+            mRefCount = refCount;
+            mCm = cm;
+        }
+
+        @Override
+        public void handleMessage(Message message) {
+            Log.d(TAG, "CM callback handler got msg " + message.what);
+            switch (message.what) {
+                case CALLBACK_PRECHECK: {
+                    NetworkRequest request = getNetworkRequest(message);
+                    NetworkCallbackListener callbacks = getCallbacks(request);
+                    if (callbacks != null) {
+                        callbacks.onPreCheck(request, getNetwork(message));
+                    } else {
+                        Log.e(TAG, "callback not found for PRECHECK message");
+                    }
+                    break;
+                }
+                case CALLBACK_AVAILABLE: {
+                    NetworkRequest request = getNetworkRequest(message);
+                    NetworkCallbackListener callbacks = getCallbacks(request);
+                    if (callbacks != null) {
+                        callbacks.onAvailable(request, getNetwork(message));
+                    } else {
+                        Log.e(TAG, "callback not found for AVAILABLE message");
+                    }
+                    break;
+                }
+                case CALLBACK_LOSING: {
+                    NetworkRequest request = getNetworkRequest(message);
+                    NetworkCallbackListener callbacks = getCallbacks(request);
+                    if (callbacks != null) {
+                        callbacks.onLosing(request, getNetwork(message), message.arg1);
+                    } else {
+                        Log.e(TAG, "callback not found for LOSING message");
+                    }
+                    break;
+                }
+                case CALLBACK_LOST: {
+                    NetworkRequest request = getNetworkRequest(message);
+                    NetworkCallbackListener callbacks = getCallbacks(request);
+                    if (callbacks != null) {
+                        callbacks.onLost(request, getNetwork(message));
+                    } else {
+                        Log.e(TAG, "callback not found for LOST message");
+                    }
+                    break;
+                }
+                case CALLBACK_UNAVAIL: {
+                    NetworkRequest req = (NetworkRequest)message.obj;
+                    NetworkCallbackListener callbacks = null;
+                    synchronized(mCallbackMap) {
+                        callbacks = mCallbackMap.get(req);
+                    }
+                    if (callbacks != null) {
+                        callbacks.onUnavailable(req);
+                    } else {
+                        Log.e(TAG, "callback not found for UNAVAIL message");
+                    }
+                    break;
+                }
+                case CALLBACK_CAP_CHANGED: {
+                    NetworkRequest request = getNetworkRequest(message);
+                    NetworkCallbackListener callbacks = getCallbacks(request);
+                    if (callbacks != null) {
+                        Network network = getNetwork(message);
+                        NetworkCapabilities cap = mCm.getNetworkCapabilities(network);
+
+                        callbacks.onNetworkCapabilitiesChanged(request, network, cap);
+                    } else {
+                        Log.e(TAG, "callback not found for CHANGED message");
+                    }
+                    break;
+                }
+                case CALLBACK_IP_CHANGED: {
+                    NetworkRequest request = getNetworkRequest(message);
+                    NetworkCallbackListener callbacks = getCallbacks(request);
+                    if (callbacks != null) {
+                        Network network = getNetwork(message);
+                        LinkProperties lp = mCm.getLinkProperties(network);
+
+                        callbacks.onLinkPropertiesChanged(request, network, lp);
+                    } else {
+                        Log.e(TAG, "callback not found for CHANGED message");
+                    }
+                    break;
+                }
+                case CALLBACK_RELEASED: {
+                    NetworkRequest req = (NetworkRequest)message.obj;
+                    NetworkCallbackListener callbacks = null;
+                    synchronized(mCallbackMap) {
+                        callbacks = mCallbackMap.remove(req);
+                    }
+                    if (callbacks != null) {
+                        callbacks.onReleased(req);
+                    } else {
+                        Log.e(TAG, "callback not found for CANCELED message");
+                    }
+                    synchronized(mRefCount) {
+                        if (mRefCount.decrementAndGet() == 0) {
+                            getLooper().quit();
+                        }
+                    }
+                    break;
+                }
+                case CALLBACK_EXIT: {
+                    Log.d(TAG, "Listener quiting");
+                    getLooper().quit();
+                    break;
+                }
+                case EXPIRE_LEGACY_REQUEST: {
+                    expireRequest((NetworkCapabilities)message.obj, message.arg1);
+                    break;
+                }
+            }
+        }
+
+        private NetworkRequest getNetworkRequest(Message msg) {
+            return (NetworkRequest)(msg.obj);
+        }
+        private NetworkCallbackListener getCallbacks(NetworkRequest req) {
+            synchronized(mCallbackMap) {
+                return mCallbackMap.get(req);
+            }
+        }
+        private Network getNetwork(Message msg) {
+            return new Network(msg.arg2);
+        }
+        private NetworkCallbackListener removeCallbacks(Message msg) {
+            NetworkRequest req = (NetworkRequest)msg.obj;
+            synchronized(mCallbackMap) {
+                return mCallbackMap.remove(req);
+            }
+        }
+    }
+
+    private void addCallbackListener() {
+        synchronized(sCallbackRefCount) {
+            if (sCallbackRefCount.incrementAndGet() == 1) {
+                // TODO - switch this over to a ManagerThread or expire it when done
+                HandlerThread callbackThread = new HandlerThread("ConnectivityManager");
+                callbackThread.start();
+                sCallbackHandler = new CallbackHandler(callbackThread.getLooper(),
+                        sNetworkCallbackListener, sCallbackRefCount, this);
+            }
+        }
+    }
+
+    private void removeCallbackListener() {
+        synchronized(sCallbackRefCount) {
+            if (sCallbackRefCount.decrementAndGet() == 0) {
+                sCallbackHandler.obtainMessage(CALLBACK_EXIT).sendToTarget();
+                sCallbackHandler = null;
+            }
+        }
+    }
+
+    static final HashMap<NetworkRequest, NetworkCallbackListener> sNetworkCallbackListener =
+            new HashMap<NetworkRequest, NetworkCallbackListener>();
+    static final AtomicInteger sCallbackRefCount = new AtomicInteger(0);
+    static CallbackHandler sCallbackHandler = null;
+
+    private final static int LISTEN  = 1;
+    private final static int REQUEST = 2;
+
+    private NetworkRequest sendRequestForNetwork(NetworkCapabilities need,
+            NetworkCallbackListener networkCallbackListener, int timeoutSec, int action,
+            int legacyType) {
+        NetworkRequest networkRequest = null;
+        if (networkCallbackListener == null) {
+            throw new IllegalArgumentException("null NetworkCallbackListener");
+        }
+        if (need == null) throw new IllegalArgumentException("null NetworkCapabilities");
+        try {
+            addCallbackListener();
+            if (action == LISTEN) {
+                networkRequest = mService.listenForNetwork(need, new Messenger(sCallbackHandler),
+                        new Binder());
+            } else {
+                networkRequest = mService.requestNetwork(need, new Messenger(sCallbackHandler),
+                        timeoutSec, new Binder(), legacyType);
+            }
+            if (networkRequest != null) {
+                synchronized(sNetworkCallbackListener) {
+                    sNetworkCallbackListener.put(networkRequest, networkCallbackListener);
+                }
+            }
+        } catch (RemoteException e) {}
+        if (networkRequest == null) removeCallbackListener();
+        return networkRequest;
+    }
+
+    /**
+     * Request a network to satisfy a set of {@link NetworkCapabilities}.
+     *
+     * This {@link NetworkRequest} will live until released via
+     * {@link #releaseNetworkRequest} or the calling application exits.
+     * Status of the request can be followed by listening to the various
+     * callbacks described in {@link NetworkCallbackListener}.  The {@link Network}
+     * can be used to direct traffic to the network.
+     *
+     * @param need {@link NetworkCapabilities} required by this request.
+     * @param networkCallbackListener The {@link NetworkCallbackListener} to be utilized for this
+     *                         request.  Note the callbacks can be shared by multiple
+     *                         requests and the NetworkRequest token utilized to
+     *                         determine to which request the callback relates.
+     * @return A {@link NetworkRequest} object identifying the request.
+     */
+    public NetworkRequest requestNetwork(NetworkCapabilities need,
+            NetworkCallbackListener networkCallbackListener) {
+        return sendRequestForNetwork(need, networkCallbackListener, 0, REQUEST, TYPE_NONE);
+    }
+
+    /**
+     * Request a network to satisfy a set of {@link NetworkCapabilities}, limited
+     * by a timeout.
+     *
+     * This function behaves identically to the non-timedout version, but if a suitable
+     * network is not found within the given time (in Seconds) the
+     * {@link NetworkCallbackListener#unavailable} callback is called.  The request must
+     * still be released normally by calling {@link releaseNetworkRequest}.
+     * @param need {@link NetworkCapabilities} required by this request.
+     * @param networkCallbackListener The callbacks to be utilized for this request.  Note
+     *                         the callbacks can be shared by multiple requests and
+     *                         the NetworkRequest token utilized to determine to which
+     *                         request the callback relates.
+     * @param timeoutSec The time in seconds to attempt looking for a suitable network
+     *                   before {@link NetworkCallbackListener#unavailable} is called.
+     * @return A {@link NetworkRequest} object identifying the request.
+     * @hide
+     */
+    public NetworkRequest requestNetwork(NetworkCapabilities need,
+            NetworkCallbackListener networkCallbackListener, int timeoutSec) {
+        return sendRequestForNetwork(need, networkCallbackListener, timeoutSec, REQUEST,
+                TYPE_NONE);
+    }
+
+    /**
+     * The maximum number of seconds the framework will look for a suitable network
+     * during a timeout-equiped call to {@link requestNetwork}.
+     * {@hide}
+     */
+    public final static int MAX_NETWORK_REQUEST_TIMEOUT_SEC = 100 * 60;
+
+    /**
+     * The lookup key for a {@link Network} object included with the intent after
+     * succesfully finding a network for the applications request.  Retrieve it with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     */
+    public static final String EXTRA_NETWORK_REQUEST_NETWORK = "networkRequestNetwork";
+
+    /**
+     * The lookup key for a {@link NetworkCapabilities} object included with the intent after
+     * succesfully finding a network for the applications request.  Retrieve it with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     */
+    public static final String EXTRA_NETWORK_REQUEST_NETWORK_CAPABILITIES =
+            "networkRequestNetworkCapabilities";
+
+
+    /**
+     * Request a network to satisfy a set of {@link NetworkCapabilities}.
+     *
+     * This function behavies identically to the callback-equiped version, but instead
+     * of {@link NetworkCallbackListener} a {@link PendingIntent} is used.  This means
+     * the request may outlive the calling application and get called back when a suitable
+     * network is found.
+     * <p>
+     * The operation is an Intent broadcast that goes to a broadcast receiver that
+     * you registered with {@link Context#registerReceiver} or through the
+     * &lt;receiver&gt; tag in an AndroidManifest.xml file
+     * <p>
+     * The operation Intent is delivered with two extras, a {@link Network} typed
+     * extra called {@link #EXTRA_NETWORK_REQUEST_NETWORK} and a {@link NetworkCapabilities}
+     * typed extra called {@link #EXTRA_NETWORK_REQUEST_NETWORK_CAPABILITIES} containing
+     * the original requests parameters.  It is important to create a new,
+     * {@link NetworkCallbackListener} based request before completing the processing of the
+     * Intent to reserve the network or it will be released shortly after the Intent
+     * is processed.
+     * <p>
+     * If there is already an request for this Intent registered (with the equality of
+     * two Intents defined by {@link Intent#filterEquals}), then it will be removed and
+     * replaced by this one, effectively releasing the previous {@link NetworkRequest}.
+     * <p>
+     * The request may be released normally by calling {@link #releaseNetworkRequest}.
+     *
+     * @param need {@link NetworkCapabilities} required by this request.
+     * @param operation Action to perform when the network is available (corresponds
+     *                  to the {@link NetworkCallbackListener#onAvailable} call.  Typically
+     *                  comes from {@link PendingIntent#getBroadcast}.
+     * @return A {@link NetworkRequest} object identifying the request.
+     */
+    public NetworkRequest requestNetwork(NetworkCapabilities need, PendingIntent operation) {
+        try {
+            return mService.pendingRequestForNetwork(need, operation);
+        } catch (RemoteException e) {}
+        return null;
+    }
+
+    /**
+     * Registers to receive notifications about all networks which satisfy the given
+     * {@link NetworkCapabilities}.  The callbacks will continue to be called until
+     * either the application exits or the request is released using
+     * {@link #releaseNetworkRequest}.
+     *
+     * @param need {@link NetworkCapabilities} required by this request.
+     * @param networkCallbackListener The {@link NetworkCallbackListener} to be called as suitable
+     *                         networks change state.
+     * @return A {@link NetworkRequest} object identifying the request.
+     */
+    public NetworkRequest listenForNetwork(NetworkCapabilities need,
+            NetworkCallbackListener networkCallbackListener) {
+        return sendRequestForNetwork(need, networkCallbackListener, 0, LISTEN, TYPE_NONE);
+    }
+
+    /**
+     * Releases a {@link NetworkRequest} generated either through a {@link #requestNetwork}
+     * or a {@link #listenForNetwork} call.  The {@link NetworkCallbackListener} given in the
+     * earlier call may continue receiving calls until the
+     * {@link NetworkCallbackListener#onReleased} function is called, signifying the end
+     * of the request.
+     *
+     * @param networkRequest The {@link NetworkRequest} generated by an earlier call to
+     *                       {@link #requestNetwork} or {@link #listenForNetwork}.
+     */
+    public void releaseNetworkRequest(NetworkRequest networkRequest) {
+        if (networkRequest == null) throw new IllegalArgumentException("null NetworkRequest");
+        try {
+            mService.releaseNetworkRequest(networkRequest);
+        } catch (RemoteException e) {}
+    }
+
+    /**
+     * Binds the current process to {@code network}.  All Sockets created in the future
+     * (and not explicitly bound via a bound SocketFactory from
+     * {@link Network#getSocketFactory() Network.getSocketFactory()}) will be bound to
+     * {@code network}.  All host name resolutions will be limited to {@code network} as well.
+     * Note that if {@code network} ever disconnects, all Sockets created in this way will cease to
+     * work and all host name resolutions will fail.  This is by design so an application doesn't
+     * accidentally use Sockets it thinks are still bound to a particular {@link Network}.
+     * To clear binding pass {@code null} for {@code network}.  Using individually bound
+     * Sockets created by Network.getSocketFactory().createSocket() and
+     * performing network-specific host name resolutions via
+     * {@link Network#getAllByName Network.getAllByName} is preferred to calling
+     * {@code setProcessDefaultNetwork}.
+     *
+     * @param network The {@link Network} to bind the current process to, or {@code null} to clear
+     *                the current binding.
+     * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid.
+     */
+    public static boolean setProcessDefaultNetwork(Network network) {
+        if (network == null) {
+            NetworkUtils.unbindProcessToNetwork();
+        } else {
+            NetworkUtils.bindProcessToNetwork(network.netId);
+        }
+        // TODO fix return value
+        return true;
+    }
+
+    /**
+     * Returns the {@link Network} currently bound to this process via
+     * {@link #setProcessDefaultNetwork}, or {@code null} if no {@link Network} is explicitly bound.
+     *
+     * @return {@code Network} to which this process is bound, or {@code null}.
+     */
+    public static Network getProcessDefaultNetwork() {
+        int netId = NetworkUtils.getNetworkBoundToProcess();
+        if (netId == 0) return null;
+        return new Network(netId);
+    }
+
+    /**
+     * Binds host resolutions performed by this process to {@code network}.
+     * {@link #setProcessDefaultNetwork} takes precedence over this setting.
+     *
+     * @param network The {@link Network} to bind host resolutions from the current process to, or
+     *                {@code null} to clear the current binding.
+     * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid.
+     * @hide
+     * @deprecated This is strictly for legacy usage to support {@link #startUsingNetworkFeature}.
+     */
+    public static boolean setProcessDefaultNetworkForHostResolution(Network network) {
+        if (network == null) {
+            NetworkUtils.unbindProcessToNetworkForHostResolution();
+        } else {
+            NetworkUtils.bindProcessToNetworkForHostResolution(network.netId);
+        }
+        // TODO hook up the return value.
+        return true;
+    }
+}
diff --git a/core/java/android/net/DhcpInfo.java b/core/java/android/net/DhcpInfo.java
new file mode 100644
index 0000000..788d7d9
--- /dev/null
+++ b/core/java/android/net/DhcpInfo.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Parcelable;
+import android.os.Parcel;
+
+/**
+ * A simple object for retrieving the results of a DHCP request.
+ */
+public class DhcpInfo implements Parcelable {
+    public int ipAddress;
+    public int gateway;
+    public int netmask;
+    public int dns1;
+    public int dns2;
+    public int serverAddress;
+
+    public int leaseDuration;
+
+    public DhcpInfo() {
+        super();
+    }
+
+    /** copy constructor {@hide} */
+    public DhcpInfo(DhcpInfo source) {
+        if (source != null) {
+            ipAddress = source.ipAddress;
+            gateway = source.gateway;
+            netmask = source.netmask;
+            dns1 = source.dns1;
+            dns2 = source.dns2;
+            serverAddress = source.serverAddress;
+            leaseDuration = source.leaseDuration;
+        }
+    }
+
+    public String toString() {
+        StringBuffer str = new StringBuffer();
+
+        str.append("ipaddr "); putAddress(str, ipAddress);
+        str.append(" gateway "); putAddress(str, gateway);
+        str.append(" netmask "); putAddress(str, netmask);
+        str.append(" dns1 "); putAddress(str, dns1);
+        str.append(" dns2 "); putAddress(str, dns2);
+        str.append(" DHCP server "); putAddress(str, serverAddress);
+        str.append(" lease ").append(leaseDuration).append(" seconds");
+
+        return str.toString();
+    }
+
+    private static void putAddress(StringBuffer buf, int addr) {
+        buf.append(NetworkUtils.intToInetAddress(addr).getHostAddress());
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(ipAddress);
+        dest.writeInt(gateway);
+        dest.writeInt(netmask);
+        dest.writeInt(dns1);
+        dest.writeInt(dns2);
+        dest.writeInt(serverAddress);
+        dest.writeInt(leaseDuration);
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public static final Creator<DhcpInfo> CREATOR =
+        new Creator<DhcpInfo>() {
+            public DhcpInfo createFromParcel(Parcel in) {
+                DhcpInfo info = new DhcpInfo();
+                info.ipAddress = in.readInt();
+                info.gateway = in.readInt();
+                info.netmask = in.readInt();
+                info.dns1 = in.readInt();
+                info.dns2 = in.readInt();
+                info.serverAddress = in.readInt();
+                info.leaseDuration = in.readInt();
+                return info;
+            }
+
+            public DhcpInfo[] newArray(int size) {
+                return new DhcpInfo[size];
+            }
+        };
+}
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
new file mode 100644
index 0000000..5f1ff3e
--- /dev/null
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -0,0 +1,175 @@
+/**
+ * Copyright (c) 2008, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.app.PendingIntent;
+import android.net.LinkQualityInfo;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkQuotaInfo;
+import android.net.NetworkRequest;
+import android.net.NetworkState;
+import android.net.ProxyInfo;
+import android.os.IBinder;
+import android.os.Messenger;
+import android.os.ParcelFileDescriptor;
+import android.os.ResultReceiver;
+
+import com.android.internal.net.LegacyVpnInfo;
+import com.android.internal.net.VpnConfig;
+import com.android.internal.net.VpnProfile;
+
+/**
+ * Interface that answers queries about, and allows changing, the
+ * state of network connectivity.
+ */
+/** {@hide} */
+interface IConnectivityManager
+{
+    // Keep this in sync with framework/native/services/connectivitymanager/ConnectivityManager.h
+    void markSocketAsUser(in ParcelFileDescriptor socket, int uid);
+
+    NetworkInfo getActiveNetworkInfo();
+    NetworkInfo getActiveNetworkInfoForUid(int uid);
+    NetworkInfo getNetworkInfo(int networkType);
+    NetworkInfo[] getAllNetworkInfo();
+
+    NetworkInfo getProvisioningOrActiveNetworkInfo();
+
+    boolean isNetworkSupported(int networkType);
+
+    LinkProperties getActiveLinkProperties();
+    LinkProperties getLinkPropertiesForType(int networkType);
+    LinkProperties getLinkProperties(in Network network);
+
+    NetworkCapabilities getNetworkCapabilities(in Network network);
+
+    NetworkState[] getAllNetworkState();
+
+    NetworkQuotaInfo getActiveNetworkQuotaInfo();
+    boolean isActiveNetworkMetered();
+
+    int startUsingNetworkFeature(int networkType, in String feature,
+            in IBinder binder);
+
+    int stopUsingNetworkFeature(int networkType, in String feature);
+
+    boolean requestRouteToHost(int networkType, int hostAddress, String packageName);
+
+    boolean requestRouteToHostAddress(int networkType, in byte[] hostAddress, String packageName);
+
+    /** Policy control over specific {@link NetworkStateTracker}. */
+    void setPolicyDataEnable(int networkType, boolean enabled);
+
+    int tether(String iface);
+
+    int untether(String iface);
+
+    int getLastTetherError(String iface);
+
+    boolean isTetheringSupported();
+
+    String[] getTetherableIfaces();
+
+    String[] getTetheredIfaces();
+
+    String[] getTetheringErroredIfaces();
+
+    String[] getTetherableUsbRegexs();
+
+    String[] getTetherableWifiRegexs();
+
+    String[] getTetherableBluetoothRegexs();
+
+    int setUsbTethering(boolean enable);
+
+    void requestNetworkTransitionWakelock(in String forWhom);
+
+    void reportInetCondition(int networkType, int percentage);
+
+    void reportBadNetwork(in Network network);
+
+    ProxyInfo getGlobalProxy();
+
+    void setGlobalProxy(in ProxyInfo p);
+
+    ProxyInfo getProxy();
+
+    void setDataDependency(int networkType, boolean met);
+
+    boolean protectVpn(in ParcelFileDescriptor socket);
+
+    boolean prepareVpn(String oldPackage, String newPackage);
+
+    ParcelFileDescriptor establishVpn(in VpnConfig config);
+
+    VpnConfig getVpnConfig();
+
+    void startLegacyVpn(in VpnProfile profile);
+
+    LegacyVpnInfo getLegacyVpnInfo();
+
+    boolean updateLockdownVpn();
+
+    void captivePortalCheckCompleted(in NetworkInfo info, boolean isCaptivePortal);
+
+    void supplyMessenger(int networkType, in Messenger messenger);
+
+    int findConnectionTypeForIface(in String iface);
+
+    int checkMobileProvisioning(int suggestedTimeOutMs);
+
+    String getMobileProvisioningUrl();
+
+    String getMobileRedirectedProvisioningUrl();
+
+    LinkQualityInfo getLinkQualityInfo(int networkType);
+
+    LinkQualityInfo getActiveLinkQualityInfo();
+
+    LinkQualityInfo[] getAllLinkQualityInfo();
+
+    void setProvisioningNotificationVisible(boolean visible, int networkType, in String extraInfo,
+            in String url);
+
+    void setAirplaneMode(boolean enable);
+
+    void registerNetworkFactory(in Messenger messenger, in String name);
+
+    void unregisterNetworkFactory(in Messenger messenger);
+
+    void registerNetworkAgent(in Messenger messenger, in NetworkInfo ni, in LinkProperties lp,
+            in NetworkCapabilities nc, int score);
+
+    NetworkRequest requestNetwork(in NetworkCapabilities networkCapabilities,
+            in Messenger messenger, int timeoutSec, in IBinder binder, int legacy);
+
+    NetworkRequest pendingRequestForNetwork(in NetworkCapabilities networkCapabilities,
+            in PendingIntent operation);
+
+    NetworkRequest listenForNetwork(in NetworkCapabilities networkCapabilities,
+            in Messenger messenger, in IBinder binder);
+
+    void pendingListenForNetwork(in NetworkCapabilities networkCapabilities,
+            in PendingIntent operation);
+
+    void releaseNetworkRequest(in NetworkRequest networkRequest);
+
+    int getRestoreDefaultNetworkDelay(int networkType);
+}
diff --git a/core/java/android/net/IpConfiguration.java b/core/java/android/net/IpConfiguration.java
new file mode 100644
index 0000000..4730bab
--- /dev/null
+++ b/core/java/android/net/IpConfiguration.java
@@ -0,0 +1,151 @@
+/*
+ * 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.LinkProperties;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A class representing a configured network.
+ * @hide
+ */
+public class IpConfiguration implements Parcelable {
+    private static final String TAG = "IpConfiguration";
+
+    public enum IpAssignment {
+        /* Use statically configured IP settings. Configuration can be accessed
+         * with linkProperties */
+        STATIC,
+        /* Use dynamically configured IP settigns */
+        DHCP,
+        /* no IP details are assigned, this is used to indicate
+         * that any existing IP settings should be retained */
+        UNASSIGNED
+    }
+
+    public IpAssignment ipAssignment;
+
+    public enum ProxySettings {
+        /* No proxy is to be used. Any existing proxy settings
+         * should be cleared. */
+        NONE,
+        /* Use statically configured proxy. Configuration can be accessed
+         * with linkProperties */
+        STATIC,
+        /* no proxy details are assigned, this is used to indicate
+         * that any existing proxy settings should be retained */
+        UNASSIGNED,
+        /* Use a Pac based proxy.
+         */
+        PAC
+    }
+
+    public ProxySettings proxySettings;
+
+    public LinkProperties linkProperties;
+
+    public IpConfiguration(IpConfiguration source) {
+        if (source != null) {
+            ipAssignment = source.ipAssignment;
+            proxySettings = source.proxySettings;
+            linkProperties = new LinkProperties(source.linkProperties);
+        } else {
+            ipAssignment = IpAssignment.UNASSIGNED;
+            proxySettings = ProxySettings.UNASSIGNED;
+            linkProperties = new LinkProperties();
+        }
+    }
+
+    public IpConfiguration() {
+         this(null);
+    }
+
+    public IpConfiguration(IpAssignment ipAssignment,
+                           ProxySettings proxySettings,
+                           LinkProperties linkProperties) {
+        this.ipAssignment = ipAssignment;
+        this.proxySettings = proxySettings;
+        this.linkProperties = new LinkProperties(linkProperties);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sbuf = new StringBuilder();
+        sbuf.append("IP assignment: " + ipAssignment.toString());
+        sbuf.append("\n");
+        sbuf.append("Proxy settings: " + proxySettings.toString());
+        sbuf.append("\n");
+        sbuf.append(linkProperties.toString());
+        sbuf.append("\n");
+
+        return sbuf.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+
+        if (!(o instanceof IpConfiguration)) {
+            return false;
+        }
+
+        IpConfiguration other = (IpConfiguration) o;
+        return this.ipAssignment == other.ipAssignment &&
+                this.proxySettings == other.proxySettings &&
+                Objects.equals(this.linkProperties, other.linkProperties);
+    }
+
+    @Override
+    public int hashCode() {
+        return 13 + (linkProperties != null ? linkProperties.hashCode() : 0) +
+               17 * ipAssignment.ordinal() +
+               47 * proxySettings.ordinal();
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface  */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(ipAssignment.name());
+        dest.writeString(proxySettings.name());
+        dest.writeParcelable(linkProperties, flags);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final Creator<IpConfiguration> CREATOR =
+        new Creator<IpConfiguration>() {
+            public IpConfiguration createFromParcel(Parcel in) {
+                IpConfiguration config = new IpConfiguration();
+                config.ipAssignment = IpAssignment.valueOf(in.readString());
+                config.proxySettings = ProxySettings.valueOf(in.readString());
+                config.linkProperties = in.readParcelable(null);
+                return config;
+            }
+
+            public IpConfiguration[] newArray(int size) {
+                return new IpConfiguration[size];
+            }
+        };
+}
diff --git a/core/java/android/net/IpPrefix.java b/core/java/android/net/IpPrefix.java
new file mode 100644
index 0000000..dfe0384
--- /dev/null
+++ b/core/java/android/net/IpPrefix.java
@@ -0,0 +1,173 @@
+/*
+ * 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.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+
+/**
+ * This class represents an IP prefix, i.e., a contiguous block of IP addresses aligned on a
+ * power of two boundary (also known as an "IP subnet"). A prefix is specified by two pieces of
+ * information:
+ *
+ * <ul>
+ * <li>A starting IP address (IPv4 or IPv6). This is the first IP address of the prefix.
+ * <li>A prefix length. This specifies the length of the prefix by specifing the number of bits
+ *     in the IP address, starting from the most significant bit in network byte order, that
+ *     are constant for all addresses in the prefix.
+ * </ul>
+ *
+ * For example, the prefix <code>192.0.2.0/24</code> covers the 256 IPv4 addresses from
+ * <code>192.0.2.0</code> to <code>192.0.2.255</code>, inclusive, and the prefix
+ * <code>2001:db8:1:2</code>  covers the 2^64 IPv6 addresses from <code>2001:db8:1:2::</code> to
+ * <code>2001:db8:1:2:ffff:ffff:ffff:ffff</code>, inclusive.
+ *
+ * Objects of this class are immutable.
+ */
+public class IpPrefix implements Parcelable {
+    private final byte[] address;  // network byte order
+    private final int prefixLength;
+
+    /**
+     * Constructs a new {@code IpPrefix} from a byte array containing an IPv4 or IPv6 address in
+     * network byte order and a prefix length.
+     *
+     * @param address the IP address. Must be non-null and exactly 4 or 16 bytes long.
+     * @param prefixLength the prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
+     *
+     * @hide
+     */
+    public IpPrefix(byte[] address, int prefixLength) {
+        if (address.length != 4 && address.length != 16) {
+            throw new IllegalArgumentException(
+                    "IpPrefix has " + address.length + " bytes which is neither 4 nor 16");
+        }
+        if (prefixLength < 0 || prefixLength > (address.length * 8)) {
+            throw new IllegalArgumentException("IpPrefix with " + address.length +
+                    " bytes has invalid prefix length " + prefixLength);
+        }
+        this.address = address.clone();
+        this.prefixLength = prefixLength;
+        // TODO: Validate that the non-prefix bits are zero
+    }
+
+    /**
+     * @hide
+     */
+    public IpPrefix(InetAddress address, int prefixLength) {
+        this(address.getAddress(), prefixLength);
+    }
+
+    /**
+     * Compares this {@code IpPrefix} object against the specified object in {@code obj}. Two
+     * objects are equal if they have the same startAddress and prefixLength.
+     *
+     * @param obj the object to be tested for equality.
+     * @return {@code true} if both objects are equal, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof IpPrefix)) {
+            return false;
+        }
+        IpPrefix that = (IpPrefix) obj;
+        return Arrays.equals(this.address, that.address) && this.prefixLength == that.prefixLength;
+    }
+
+    /**
+     * Gets the hashcode of the represented IP prefix.
+     *
+     * @return the appropriate hashcode value.
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(address) + 11 * prefixLength;
+    }
+
+    /**
+     * Returns a copy of the first IP address in the prefix. Modifying the returned object does not
+     * change this object's contents.
+     *
+     * @return the address in the form of a byte array.
+     */
+    public InetAddress getAddress() {
+        try {
+            return InetAddress.getByAddress(address);
+        } catch (UnknownHostException e) {
+            // Cannot happen. InetAddress.getByAddress can only throw an exception if the byte
+            // array is the wrong length, but we check that in the constructor.
+            return null;
+        }
+    }
+
+    /**
+     * Returns a copy of the IP address bytes in network order (the highest order byte is the zeroth
+     * element). Modifying the returned array does not change this object's contents.
+     *
+     * @return the address in the form of a byte array.
+     */
+    public byte[] getRawAddress() {
+        return address.clone();
+    }
+
+    /**
+     * Returns the prefix length of this {@code IpAddress}.
+     *
+     * @return the prefix length.
+     */
+    public int getPrefixLength() {
+        return prefixLength;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(address);
+        dest.writeInt(prefixLength);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public static final Creator<IpPrefix> CREATOR =
+            new Creator<IpPrefix>() {
+                public IpPrefix createFromParcel(Parcel in) {
+                    byte[] address = in.createByteArray();
+                    int prefixLength = in.readInt();
+                    return new IpPrefix(address, prefixLength);
+                }
+
+                public IpPrefix[] newArray(int size) {
+                    return new IpPrefix[size];
+                }
+            };
+}
diff --git a/core/java/android/net/LinkAddress.java b/core/java/android/net/LinkAddress.java
new file mode 100644
index 0000000..5246078
--- /dev/null
+++ b/core/java/android/net/LinkAddress.java
@@ -0,0 +1,332 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.UnknownHostException;
+
+import static android.system.OsConstants.IFA_F_DADFAILED;
+import static android.system.OsConstants.IFA_F_DEPRECATED;
+import static android.system.OsConstants.IFA_F_TENTATIVE;
+import static android.system.OsConstants.RT_SCOPE_HOST;
+import static android.system.OsConstants.RT_SCOPE_LINK;
+import static android.system.OsConstants.RT_SCOPE_SITE;
+import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
+
+/**
+ * Identifies an IP address on a network link.
+ *
+ * A {@code LinkAddress} consists of:
+ * <ul>
+ * <li>An IP address and prefix length (e.g., {@code 2001:db8::1/64} or {@code 192.0.2.1/24}).
+ * The address must be unicast, as multicast addresses cannot be assigned to interfaces.
+ * <li>Address flags: A bitmask of {@code OsConstants.IFA_F_*} values representing properties
+ * of the address (e.g., {@code android.system.OsConstants.IFA_F_OPTIMISTIC}).
+ * <li>Address scope: One of the {@code OsConstants.IFA_F_*} values; defines the scope in which
+ * the address is unique (e.g.,
+ * {@code android.system.OsConstants.RT_SCOPE_LINK} or
+ * {@code android.system.OsConstants.RT_SCOPE_UNIVERSE}).
+ * </ul>
+ */
+public class LinkAddress implements Parcelable {
+    /**
+     * IPv4 or IPv6 address.
+     */
+    private InetAddress address;
+
+    /**
+     * Prefix length.
+     */
+    private int prefixLength;
+
+    /**
+     * Address flags. A bitmask of IFA_F_* values.
+     */
+    private int flags;
+
+    /**
+     * Address scope. One of the RT_SCOPE_* constants.
+     */
+    private int scope;
+
+    /**
+     * Utility function to determines the scope of a unicast address. Per RFC 4291 section 2.5 and
+     * RFC 6724 section 3.2.
+     * @hide
+     */
+    static int scopeForUnicastAddress(InetAddress addr) {
+        if (addr.isAnyLocalAddress()) {
+            return RT_SCOPE_HOST;
+        }
+
+        if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
+            return RT_SCOPE_LINK;
+        }
+
+        // isSiteLocalAddress() returns true for private IPv4 addresses, but RFC 6724 section 3.2
+        // says that they are assigned global scope.
+        if (!(addr instanceof Inet4Address) && addr.isSiteLocalAddress()) {
+            return RT_SCOPE_SITE;
+        }
+
+        return RT_SCOPE_UNIVERSE;
+    }
+
+    /**
+     * Utility function for the constructors.
+     */
+    private void init(InetAddress address, int prefixLength, int flags, int scope) {
+        if (address == null ||
+                address.isMulticastAddress() ||
+                prefixLength < 0 ||
+                ((address instanceof Inet4Address) && prefixLength > 32) ||
+                (prefixLength > 128)) {
+            throw new IllegalArgumentException("Bad LinkAddress params " + address +
+                    "/" + prefixLength);
+        }
+        this.address = address;
+        this.prefixLength = prefixLength;
+        this.flags = flags;
+        this.scope = scope;
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from an {@code InetAddress} and prefix length, with
+     * the specified flags and scope. Flags and scope are not checked for validity.
+     * @param address The IP address.
+     * @param prefixLength The prefix length.
+     * @param flags A bitmask of {@code IFA_F_*} values representing properties of the address.
+     * @param scope An integer defining the scope in which the address is unique (e.g.,
+     *              {@link OsConstants#RT_SCOPE_LINK} or {@link OsConstants#RT_SCOPE_SITE}).
+     * @hide
+     */
+    public LinkAddress(InetAddress address, int prefixLength, int flags, int scope) {
+        init(address, prefixLength, flags, scope);
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from an {@code InetAddress} and a prefix length.
+     * The flags are set to zero and the scope is determined from the address.
+     * @param address The IP address.
+     * @param prefixLength The prefix length.
+     * @hide
+     */
+    public LinkAddress(InetAddress address, int prefixLength) {
+        this(address, prefixLength, 0, 0);
+        this.scope = scopeForUnicastAddress(address);
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from an {@code InterfaceAddress}.
+     * The flags are set to zero and the scope is determined from the address.
+     * @param interfaceAddress The interface address.
+     * @hide
+     */
+    public LinkAddress(InterfaceAddress interfaceAddress) {
+        this(interfaceAddress.getAddress(),
+             interfaceAddress.getNetworkPrefixLength());
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from a string such as "192.0.2.5/24" or
+     * "2001:db8::1/64". The flags are set to zero and the scope is determined from the address.
+     * @param string The string to parse.
+     * @hide
+     */
+    public LinkAddress(String address) {
+        this(address, 0, 0);
+        this.scope = scopeForUnicastAddress(this.address);
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from a string such as "192.0.2.5/24" or
+     * "2001:db8::1/64", with the specified flags and scope.
+     * @param string The string to parse.
+     * @param flags The address flags.
+     * @param scope The address scope.
+     * @hide
+     */
+    public LinkAddress(String address, int flags, int scope) {
+        InetAddress inetAddress = null;
+        int prefixLength = -1;
+        try {
+            String [] pieces = address.split("/", 2);
+            prefixLength = Integer.parseInt(pieces[1]);
+            inetAddress = InetAddress.parseNumericAddress(pieces[0]);
+        } catch (NullPointerException e) {            // Null string.
+        } catch (ArrayIndexOutOfBoundsException e) {  // No prefix length.
+        } catch (NumberFormatException e) {           // Non-numeric prefix.
+        } catch (IllegalArgumentException e) {        // Invalid IP address.
+        }
+
+        if (inetAddress == null || prefixLength == -1) {
+            throw new IllegalArgumentException("Bad LinkAddress params " + address);
+        }
+
+        init(inetAddress, prefixLength, flags, scope);
+    }
+
+    /**
+     * Returns a string representation of this address, such as "192.0.2.1/24" or "2001:db8::1/64".
+     * The string representation does not contain the flags and scope, just the address and prefix
+     * length.
+     */
+    @Override
+    public String toString() {
+        return address.getHostAddress() + "/" + prefixLength;
+    }
+
+    /**
+     * Compares this {@code LinkAddress} instance against {@code obj}. Two addresses are equal if
+     * their address, prefix length, flags and scope are equal. Thus, for example, two addresses
+     * that have the same address and prefix length are not equal if one of them is deprecated and
+     * the other is not.
+     *
+     * @param obj the object to be tested for equality.
+     * @return {@code true} if both objects are equal, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof LinkAddress)) {
+            return false;
+        }
+        LinkAddress linkAddress = (LinkAddress) obj;
+        return this.address.equals(linkAddress.address) &&
+            this.prefixLength == linkAddress.prefixLength &&
+            this.flags == linkAddress.flags &&
+            this.scope == linkAddress.scope;
+    }
+
+    /**
+     * Returns a hashcode for this address.
+     */
+    @Override
+    public int hashCode() {
+        return address.hashCode() + 11 * prefixLength + 19 * flags + 43 * scope;
+    }
+
+    /**
+     * Determines whether this {@code LinkAddress} and the provided {@code LinkAddress}
+     * represent the same address. Two {@code LinkAddresses} represent the same address
+     * if they have the same IP address and prefix length, even if their properties are
+     * different.
+     *
+     * @param other the {@code LinkAddress} to compare to.
+     * @return {@code true} if both objects have the same address and prefix length, {@code false}
+     * otherwise.
+     * @hide
+     */
+    public boolean isSameAddressAs(LinkAddress other) {
+        return address.equals(other.address) && prefixLength == other.prefixLength;
+    }
+
+    /**
+     * Returns the {@link InetAddress} of this {@code LinkAddress}.
+     */
+    public InetAddress getAddress() {
+        return address;
+    }
+
+    /**
+     * Returns the prefix length of this {@code LinkAddress}.
+     */
+    public int getPrefixLength() {
+        return prefixLength;
+    }
+
+    /**
+     * Returns the prefix length of this {@code LinkAddress}.
+     * TODO: Delete all callers and remove in favour of getPrefixLength().
+     * @hide
+     */
+    public int getNetworkPrefixLength() {
+        return getPrefixLength();
+    }
+
+    /**
+     * Returns the flags of this {@code LinkAddress}.
+     */
+    public int getFlags() {
+        return flags;
+    }
+
+    /**
+     * Returns the scope of this {@code LinkAddress}.
+     */
+    public int getScope() {
+        return scope;
+    }
+
+    /**
+     * Returns true if this {@code LinkAddress} is global scope and preferred.
+     * @hide
+     */
+    public boolean isGlobalPreferred() {
+        return (scope == RT_SCOPE_UNIVERSE &&
+                (flags & (IFA_F_DADFAILED | IFA_F_DEPRECATED | IFA_F_TENTATIVE)) == 0L);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(address.getAddress());
+        dest.writeInt(prefixLength);
+        dest.writeInt(this.flags);
+        dest.writeInt(scope);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public static final Creator<LinkAddress> CREATOR =
+        new Creator<LinkAddress>() {
+            public LinkAddress createFromParcel(Parcel in) {
+                InetAddress address = null;
+                try {
+                    address = InetAddress.getByAddress(in.createByteArray());
+                } catch (UnknownHostException e) {
+                    // Nothing we can do here. When we call the constructor, we'll throw an
+                    // IllegalArgumentException, because a LinkAddress can't have a null
+                    // InetAddress.
+                }
+                int prefixLength = in.readInt();
+                int flags = in.readInt();
+                int scope = in.readInt();
+                return new LinkAddress(address, prefixLength, flags, scope);
+            }
+
+            public LinkAddress[] newArray(int size) {
+                return new LinkAddress[size];
+            }
+        };
+}
diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java
new file mode 100644
index 0000000..bb05936
--- /dev/null
+++ b/core/java/android/net/LinkProperties.java
@@ -0,0 +1,880 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+import android.net.ProxyInfo;
+import android.os.Parcelable;
+import android.os.Parcel;
+import android.text.TextUtils;
+
+import java.net.InetAddress;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+
+/**
+ * Describes the properties of a network link.
+ *
+ * A link represents a connection to a network.
+ * It may have multiple addresses and multiple gateways,
+ * multiple dns servers but only one http proxy and one
+ * network interface.
+ *
+ * Note that this is just a holder of data.  Modifying it
+ * does not affect live networks.
+ *
+ */
+public class LinkProperties implements Parcelable {
+    // The interface described by the network link.
+    private String mIfaceName;
+    private ArrayList<LinkAddress> mLinkAddresses = new ArrayList<LinkAddress>();
+    private ArrayList<InetAddress> mDnses = new ArrayList<InetAddress>();
+    private String mDomains;
+    private ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
+    private ProxyInfo mHttpProxy;
+    private int mMtu;
+
+    // Stores the properties of links that are "stacked" above this link.
+    // Indexed by interface name to allow modification and to prevent duplicates being added.
+    private Hashtable<String, LinkProperties> mStackedLinks =
+        new Hashtable<String, LinkProperties>();
+
+    /**
+     * @hide
+     */
+    public static class CompareResult<T> {
+        public List<T> removed = new ArrayList<T>();
+        public List<T> added = new ArrayList<T>();
+
+        @Override
+        public String toString() {
+            String retVal = "removed=[";
+            for (T addr : removed) retVal += addr.toString() + ",";
+            retVal += "] added=[";
+            for (T addr : added) retVal += addr.toString() + ",";
+            retVal += "]";
+            return retVal;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public LinkProperties() {
+    }
+
+    /**
+     * @hide
+     */
+    public LinkProperties(LinkProperties source) {
+        if (source != null) {
+            mIfaceName = source.getInterfaceName();
+            for (LinkAddress l : source.getLinkAddresses()) mLinkAddresses.add(l);
+            for (InetAddress i : source.getDnsServers()) mDnses.add(i);
+            mDomains = source.getDomains();
+            for (RouteInfo r : source.getRoutes()) mRoutes.add(r);
+            mHttpProxy = (source.getHttpProxy() == null)  ?
+                    null : new ProxyInfo(source.getHttpProxy());
+            for (LinkProperties l: source.mStackedLinks.values()) {
+                addStackedLink(l);
+            }
+            setMtu(source.getMtu());
+        }
+    }
+
+    /**
+     * Sets the interface name for this link.  All {@link RouteInfo} already set for this
+     * will have their interface changed to match this new value.
+     *
+     * @param iface The name of the network interface used for this link.
+     * @hide
+     */
+    public void setInterfaceName(String iface) {
+        mIfaceName = iface;
+        ArrayList<RouteInfo> newRoutes = new ArrayList<RouteInfo>(mRoutes.size());
+        for (RouteInfo route : mRoutes) {
+            newRoutes.add(routeWithInterface(route));
+        }
+        mRoutes = newRoutes;
+    }
+
+    /**
+     * Gets the interface name for this link.  May be {@code null} if not set.
+     *
+     * @return The interface name set for this link or {@code null}.
+     */
+    public String getInterfaceName() {
+        return mIfaceName;
+    }
+
+    /**
+     * @hide
+     */
+    public List<String> getAllInterfaceNames() {
+        List<String> interfaceNames = new ArrayList<String>(mStackedLinks.size() + 1);
+        if (mIfaceName != null) interfaceNames.add(new String(mIfaceName));
+        for (LinkProperties stacked: mStackedLinks.values()) {
+            interfaceNames.addAll(stacked.getAllInterfaceNames());
+        }
+        return interfaceNames;
+    }
+
+    /**
+     * Returns all the addresses on this link.  We often think of a link having a single address,
+     * however, particularly with Ipv6 several addresses are typical.  Note that the
+     * {@code LinkProperties} actually contains {@link LinkAddress} objects which also include
+     * prefix lengths for each address.  This is a simplified utility alternative to
+     * {@link LinkProperties#getLinkAddresses}.
+     *
+     * @return An umodifiable {@link List} of {@link InetAddress} for this link.
+     * @hide
+     */
+    public List<InetAddress> getAddresses() {
+        List<InetAddress> addresses = new ArrayList<InetAddress>();
+        for (LinkAddress linkAddress : mLinkAddresses) {
+            addresses.add(linkAddress.getAddress());
+        }
+        return Collections.unmodifiableList(addresses);
+    }
+
+    /**
+     * Returns all the addresses on this link and all the links stacked above it.
+     * @hide
+     */
+    public List<InetAddress> getAllAddresses() {
+        List<InetAddress> addresses = new ArrayList<InetAddress>();
+        for (LinkAddress linkAddress : mLinkAddresses) {
+            addresses.add(linkAddress.getAddress());
+        }
+        for (LinkProperties stacked: mStackedLinks.values()) {
+            addresses.addAll(stacked.getAllAddresses());
+        }
+        return addresses;
+    }
+
+    private int findLinkAddressIndex(LinkAddress address) {
+        for (int i = 0; i < mLinkAddresses.size(); i++) {
+            if (mLinkAddresses.get(i).isSameAddressAs(address)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Adds a {@link LinkAddress} to this {@code LinkProperties} if a {@link LinkAddress} of the
+     * same address/prefix does not already exist.  If it does exist it is replaced.
+     * @param address The {@code LinkAddress} to add.
+     * @return true if {@code address} was added or updated, false otherwise.
+     * @hide
+     */
+    public boolean addLinkAddress(LinkAddress address) {
+        if (address == null) {
+            return false;
+        }
+        int i = findLinkAddressIndex(address);
+        if (i < 0) {
+            // Address was not present. Add it.
+            mLinkAddresses.add(address);
+            return true;
+        } else if (mLinkAddresses.get(i).equals(address)) {
+            // Address was present and has same properties. Do nothing.
+            return false;
+        } else {
+            // Address was present and has different properties. Update it.
+            mLinkAddresses.set(i, address);
+            return true;
+        }
+    }
+
+    /**
+     * Removes a {@link LinkAddress} from this {@code LinkProperties}.  Specifically, matches
+     * and {@link LinkAddress} with the same address and prefix.
+     *
+     * @param toRemove A {@link LinkAddress} specifying the address to remove.
+     * @return true if the address was removed, false if it did not exist.
+     * @hide
+     */
+    public boolean removeLinkAddress(LinkAddress toRemove) {
+        int i = findLinkAddressIndex(toRemove);
+        if (i >= 0) {
+            mLinkAddresses.remove(i);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns all the {@link LinkAddress} on this link.  Typically a link will have
+     * one IPv4 address and one or more IPv6 addresses.
+     *
+     * @return An unmodifiable {@link List} of {@link LinkAddress} for this link.
+     */
+    public List<LinkAddress> getLinkAddresses() {
+        return Collections.unmodifiableList(mLinkAddresses);
+    }
+
+    /**
+     * Returns all the addresses on this link and all the links stacked above it.
+     * @hide
+     */
+    public List<LinkAddress> getAllLinkAddresses() {
+        List<LinkAddress> addresses = new ArrayList<LinkAddress>();
+        addresses.addAll(mLinkAddresses);
+        for (LinkProperties stacked: mStackedLinks.values()) {
+            addresses.addAll(stacked.getAllLinkAddresses());
+        }
+        return addresses;
+    }
+
+    /**
+     * Replaces the {@link LinkAddress} in this {@code LinkProperties} with
+     * the given {@link Collection} of {@link LinkAddress}.
+     *
+     * @param addresses The {@link Collection} of {@link LinkAddress} to set in this
+     *                  object.
+     * @hide
+     */
+    public void setLinkAddresses(Collection<LinkAddress> addresses) {
+        mLinkAddresses.clear();
+        for (LinkAddress address: addresses) {
+            addLinkAddress(address);
+        }
+    }
+
+    /**
+     * Adds the given {@link InetAddress} to the list of DNS servers.
+     *
+     * @param dnsServer The {@link InetAddress} to add to the list of DNS servers.
+     * @hide
+     */
+    public void addDnsServer(InetAddress dnsServer) {
+        if (dnsServer != null) mDnses.add(dnsServer);
+    }
+
+    /**
+     * Returns all the {@link InetAddress} for DNS servers on this link.
+     *
+     * @return An umodifiable {@link List} of {@link InetAddress} for DNS servers on
+     *         this link.
+     */
+    public List<InetAddress> getDnsServers() {
+        return Collections.unmodifiableList(mDnses);
+    }
+
+    /**
+     * Sets the DNS domain search path used on this link.
+     *
+     * @param domains A {@link String} listing in priority order the comma separated
+     *                domains to search when resolving host names on this link.
+     * @hide
+     */
+    public void setDomains(String domains) {
+        mDomains = domains;
+    }
+
+    /**
+     * Get the DNS domains search path set for this link.
+     *
+     * @return A {@link String} containing the comma separated domains to search when resolving
+     *         host names on this link.
+     */
+    public String getDomains() {
+        return mDomains;
+    }
+
+    /**
+     * Sets the Maximum Transmission Unit size to use on this link.  This should not be used
+     * unless the system default (1500) is incorrect.  Values less than 68 or greater than
+     * 10000 will be ignored.
+     *
+     * @param mtu The MTU to use for this link.
+     * @hide
+     */
+    public void setMtu(int mtu) {
+        mMtu = mtu;
+    }
+
+    /**
+     * Gets any non-default MTU size set for this link.  Note that if the default is being used
+     * this will return 0.
+     *
+     * @return The mtu value set for this link.
+     * @hide
+     */
+    public int getMtu() {
+        return mMtu;
+    }
+
+    private RouteInfo routeWithInterface(RouteInfo route) {
+        return new RouteInfo(
+            route.getDestination(),
+            route.getGateway(),
+            mIfaceName);
+    }
+
+    /**
+     * Adds a {@link RouteInfo} to this {@code LinkProperties}.  If the {@link RouteInfo}
+     * had an interface name set and that differs from the interface set for this
+     * {@code LinkProperties} an {@link IllegalArgumentException} will be thrown.  The
+     * proper course is to add either un-named or properly named {@link RouteInfo}.
+     *
+     * @param route A {@link RouteInfo} to add to this object.
+     * @hide
+     */
+    public void addRoute(RouteInfo route) {
+        if (route != null) {
+            String routeIface = route.getInterface();
+            if (routeIface != null && !routeIface.equals(mIfaceName)) {
+                throw new IllegalArgumentException(
+                   "Route added with non-matching interface: " + routeIface +
+                   " vs. " + mIfaceName);
+            }
+            mRoutes.add(routeWithInterface(route));
+        }
+    }
+
+    /**
+     * Returns all the {@link RouteInfo} set on this link.
+     *
+     * @return An unmodifiable {@link List} of {@link RouteInfo} for this link.
+     */
+    public List<RouteInfo> getRoutes() {
+        return Collections.unmodifiableList(mRoutes);
+    }
+
+    /**
+     * Returns all the routes on this link and all the links stacked above it.
+     * @hide
+     */
+    public List<RouteInfo> getAllRoutes() {
+        List<RouteInfo> routes = new ArrayList();
+        routes.addAll(mRoutes);
+        for (LinkProperties stacked: mStackedLinks.values()) {
+            routes.addAll(stacked.getAllRoutes());
+        }
+        return routes;
+    }
+
+    /**
+     * Sets the recommended {@link ProxyInfo} to use on this link, or {@code null} for none.
+     * Note that Http Proxies are only a hint - the system recommends their use, but it does
+     * not enforce it and applications may ignore them.
+     *
+     * @param proxy A {@link ProxyInfo} defining the Http Proxy to use on this link.
+     * @hide
+     */
+    public void setHttpProxy(ProxyInfo proxy) {
+        mHttpProxy = proxy;
+    }
+
+    /**
+     * Gets the recommended {@link ProxyInfo} (or {@code null}) set on this link.
+     *
+     * @return The {@link ProxyInfo} set on this link
+     */
+    public ProxyInfo getHttpProxy() {
+        return mHttpProxy;
+    }
+
+    /**
+     * Adds a stacked link.
+     *
+     * If there is already a stacked link with the same interfacename as link,
+     * that link is replaced with link. Otherwise, link is added to the list
+     * of stacked links. If link is null, nothing changes.
+     *
+     * @param link The link to add.
+     * @return true if the link was stacked, false otherwise.
+     * @hide
+     */
+    public boolean addStackedLink(LinkProperties link) {
+        if (link != null && link.getInterfaceName() != null) {
+            mStackedLinks.put(link.getInterfaceName(), link);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes a stacked link.
+     *
+     * If there a stacked link with the same interfacename as link, it is
+     * removed. Otherwise, nothing changes.
+     *
+     * @param link The link to remove.
+     * @return true if the link was removed, false otherwise.
+     * @hide
+     */
+    public boolean removeStackedLink(LinkProperties link) {
+        if (link != null && link.getInterfaceName() != null) {
+            LinkProperties removed = mStackedLinks.remove(link.getInterfaceName());
+            return removed != null;
+        }
+        return false;
+    }
+
+    /**
+     * Returns all the links stacked on top of this link.
+     * @hide
+     */
+    public List<LinkProperties> getStackedLinks() {
+        List<LinkProperties> stacked = new ArrayList<LinkProperties>();
+        for (LinkProperties link : mStackedLinks.values()) {
+          stacked.add(new LinkProperties(link));
+        }
+        return Collections.unmodifiableList(stacked);
+    }
+
+    /**
+     * Clears this object to its initial state.
+     * @hide
+     */
+    public void clear() {
+        mIfaceName = null;
+        mLinkAddresses.clear();
+        mDnses.clear();
+        mDomains = null;
+        mRoutes.clear();
+        mHttpProxy = null;
+        mStackedLinks.clear();
+        mMtu = 0;
+    }
+
+    /**
+     * Implement the Parcelable interface
+     * @hide
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        String ifaceName = (mIfaceName == null ? "" : "InterfaceName: " + mIfaceName + " ");
+
+        String linkAddresses = "LinkAddresses: [";
+        for (LinkAddress addr : mLinkAddresses) linkAddresses += addr.toString() + ",";
+        linkAddresses += "] ";
+
+        String dns = "DnsAddresses: [";
+        for (InetAddress addr : mDnses) dns += addr.getHostAddress() + ",";
+        dns += "] ";
+
+        String domainName = "Domains: " + mDomains;
+
+        String mtu = " MTU: " + mMtu;
+
+        String routes = " Routes: [";
+        for (RouteInfo route : mRoutes) routes += route.toString() + ",";
+        routes += "] ";
+        String proxy = (mHttpProxy == null ? "" : " HttpProxy: " + mHttpProxy.toString() + " ");
+
+        String stacked = "";
+        if (mStackedLinks.values().size() > 0) {
+            stacked += " Stacked: [";
+            for (LinkProperties link: mStackedLinks.values()) {
+                stacked += " [" + link.toString() + " ],";
+            }
+            stacked += "] ";
+        }
+        return "{" + ifaceName + linkAddresses + routes + dns + domainName + mtu
+            + proxy + stacked + "}";
+    }
+
+    /**
+     * Returns true if this link has an IPv4 address.
+     *
+     * @return {@code true} if there is an IPv4 address, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasIPv4Address() {
+        for (LinkAddress address : mLinkAddresses) {
+          if (address.getAddress() instanceof Inet4Address) {
+            return true;
+          }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if this link has an IPv6 address.
+     *
+     * @return {@code true} if there is an IPv6 address, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasIPv6Address() {
+        for (LinkAddress address : mLinkAddresses) {
+          if (address.getAddress() instanceof Inet6Address) {
+            return true;
+          }
+        }
+        return false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} interface name against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalInterfaceName(LinkProperties target) {
+        return TextUtils.equals(getInterfaceName(), target.getInterfaceName());
+    }
+
+    /**
+     * Compares this {@code LinkProperties} interface addresses against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalAddresses(LinkProperties target) {
+        Collection<InetAddress> targetAddresses = target.getAddresses();
+        Collection<InetAddress> sourceAddresses = getAddresses();
+        return (sourceAddresses.size() == targetAddresses.size()) ?
+                    sourceAddresses.containsAll(targetAddresses) : false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} DNS addresses against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalDnses(LinkProperties target) {
+        Collection<InetAddress> targetDnses = target.getDnsServers();
+        String targetDomains = target.getDomains();
+        if (mDomains == null) {
+            if (targetDomains != null) return false;
+        } else {
+            if (mDomains.equals(targetDomains) == false) return false;
+        }
+        return (mDnses.size() == targetDnses.size()) ?
+                    mDnses.containsAll(targetDnses) : false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} Routes against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalRoutes(LinkProperties target) {
+        Collection<RouteInfo> targetRoutes = target.getRoutes();
+        return (mRoutes.size() == targetRoutes.size()) ?
+                    mRoutes.containsAll(targetRoutes) : false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} HttpProxy against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalHttpProxy(LinkProperties target) {
+        return getHttpProxy() == null ? target.getHttpProxy() == null :
+                    getHttpProxy().equals(target.getHttpProxy());
+    }
+
+    /**
+     * Compares this {@code LinkProperties} stacked links against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalStackedLinks(LinkProperties target) {
+        if (!mStackedLinks.keySet().equals(target.mStackedLinks.keySet())) {
+            return false;
+        }
+        for (LinkProperties stacked : mStackedLinks.values()) {
+            // Hashtable values can never be null.
+            String iface = stacked.getInterfaceName();
+            if (!stacked.equals(target.mStackedLinks.get(iface))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} MTU against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalMtu(LinkProperties target) {
+        return getMtu() == target.getMtu();
+    }
+
+    @Override
+    /**
+     * Compares this {@code LinkProperties} instance against the target
+     * LinkProperties in {@code obj}. Two LinkPropertieses are equal if
+     * all their fields are equal in values.
+     *
+     * For collection fields, such as mDnses, containsAll() is used to check
+     * if two collections contains the same elements, independent of order.
+     * There are two thoughts regarding containsAll()
+     * 1. Duplicated elements. eg, (A, B, B) and (A, A, B) are equal.
+     * 2. Worst case performance is O(n^2).
+     *
+     * @param obj the object to be tested for equality.
+     * @return {@code true} if both objects are equal, {@code false} otherwise.
+     */
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof LinkProperties)) return false;
+
+        LinkProperties target = (LinkProperties) obj;
+        /**
+         * This method does not check that stacked interfaces are equal, because
+         * stacked interfaces are not so much a property of the link as a
+         * description of connections between links.
+         */
+        return isIdenticalInterfaceName(target) &&
+                isIdenticalAddresses(target) &&
+                isIdenticalDnses(target) &&
+                isIdenticalRoutes(target) &&
+                isIdenticalHttpProxy(target) &&
+                isIdenticalStackedLinks(target) &&
+                isIdenticalMtu(target);
+    }
+
+    /**
+     * Compares the addresses in this LinkProperties with another
+     * LinkProperties, examining only addresses on the base link.
+     *
+     * @param target a LinkProperties with the new list of addresses
+     * @return the differences between the addresses.
+     * @hide
+     */
+    public CompareResult<LinkAddress> compareAddresses(LinkProperties target) {
+        /*
+         * Duplicate the LinkAddresses into removed, we will be removing
+         * address which are common between mLinkAddresses and target
+         * leaving the addresses that are different. And address which
+         * are in target but not in mLinkAddresses are placed in the
+         * addedAddresses.
+         */
+        CompareResult<LinkAddress> result = new CompareResult<LinkAddress>();
+        result.removed = new ArrayList<LinkAddress>(mLinkAddresses);
+        result.added.clear();
+        if (target != null) {
+            for (LinkAddress newAddress : target.getLinkAddresses()) {
+                if (! result.removed.remove(newAddress)) {
+                    result.added.add(newAddress);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Compares the DNS addresses in this LinkProperties with another
+     * LinkProperties, examining only DNS addresses on the base link.
+     *
+     * @param target a LinkProperties with the new list of dns addresses
+     * @return the differences between the DNS addresses.
+     * @hide
+     */
+    public CompareResult<InetAddress> compareDnses(LinkProperties target) {
+        /*
+         * Duplicate the InetAddresses into removed, we will be removing
+         * dns address which are common between mDnses and target
+         * leaving the addresses that are different. And dns address which
+         * are in target but not in mDnses are placed in the
+         * addedAddresses.
+         */
+        CompareResult<InetAddress> result = new CompareResult<InetAddress>();
+
+        result.removed = new ArrayList<InetAddress>(mDnses);
+        result.added.clear();
+        if (target != null) {
+            for (InetAddress newAddress : target.getDnsServers()) {
+                if (! result.removed.remove(newAddress)) {
+                    result.added.add(newAddress);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Compares all routes in this LinkProperties with another LinkProperties,
+     * examining both the the base link and all stacked links.
+     *
+     * @param target a LinkProperties with the new list of routes
+     * @return the differences between the routes.
+     * @hide
+     */
+    public CompareResult<RouteInfo> compareAllRoutes(LinkProperties target) {
+        /*
+         * Duplicate the RouteInfos into removed, we will be removing
+         * routes which are common between mRoutes and target
+         * leaving the routes that are different. And route address which
+         * are in target but not in mRoutes are placed in added.
+         */
+        CompareResult<RouteInfo> result = new CompareResult<RouteInfo>();
+
+        result.removed = getAllRoutes();
+        result.added.clear();
+        if (target != null) {
+            for (RouteInfo r : target.getAllRoutes()) {
+                if (! result.removed.remove(r)) {
+                    result.added.add(r);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Compares all interface names in this LinkProperties with another
+     * LinkProperties, examining both the the base link and all stacked links.
+     *
+     * @param target a LinkProperties with the new list of interface names
+     * @return the differences between the interface names.
+     * @hide
+     */
+    public CompareResult<String> compareAllInterfaceNames(LinkProperties target) {
+        /*
+         * Duplicate the interface names into removed, we will be removing
+         * interface names which are common between this and target
+         * leaving the interface names that are different. And interface names which
+         * are in target but not in this are placed in added.
+         */
+        CompareResult<String> result = new CompareResult<String>();
+
+        result.removed = getAllInterfaceNames();
+        result.added.clear();
+        if (target != null) {
+            for (String r : target.getAllInterfaceNames()) {
+                if (! result.removed.remove(r)) {
+                    result.added.add(r);
+                }
+            }
+        }
+        return result;
+    }
+
+
+    @Override
+    /**
+     * generate hashcode based on significant fields
+     * Equal objects must produce the same hash code, while unequal objects
+     * may have the same hash codes.
+     */
+    public int hashCode() {
+        return ((null == mIfaceName) ? 0 : mIfaceName.hashCode()
+                + mLinkAddresses.size() * 31
+                + mDnses.size() * 37
+                + ((null == mDomains) ? 0 : mDomains.hashCode())
+                + mRoutes.size() * 41
+                + ((null == mHttpProxy) ? 0 : mHttpProxy.hashCode())
+                + mStackedLinks.hashCode() * 47)
+                + mMtu * 51;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(getInterfaceName());
+        dest.writeInt(mLinkAddresses.size());
+        for(LinkAddress linkAddress : mLinkAddresses) {
+            dest.writeParcelable(linkAddress, flags);
+        }
+
+        dest.writeInt(mDnses.size());
+        for(InetAddress d : mDnses) {
+            dest.writeByteArray(d.getAddress());
+        }
+        dest.writeString(mDomains);
+        dest.writeInt(mMtu);
+        dest.writeInt(mRoutes.size());
+        for(RouteInfo route : mRoutes) {
+            dest.writeParcelable(route, flags);
+        }
+
+        if (mHttpProxy != null) {
+            dest.writeByte((byte)1);
+            dest.writeParcelable(mHttpProxy, flags);
+        } else {
+            dest.writeByte((byte)0);
+        }
+        ArrayList<LinkProperties> stackedLinks = new ArrayList(mStackedLinks.values());
+        dest.writeList(stackedLinks);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public static final Creator<LinkProperties> CREATOR =
+        new Creator<LinkProperties>() {
+            public LinkProperties createFromParcel(Parcel in) {
+                LinkProperties netProp = new LinkProperties();
+
+                String iface = in.readString();
+                if (iface != null) {
+                    netProp.setInterfaceName(iface);
+                }
+                int addressCount = in.readInt();
+                for (int i=0; i<addressCount; i++) {
+                    netProp.addLinkAddress((LinkAddress)in.readParcelable(null));
+                }
+                addressCount = in.readInt();
+                for (int i=0; i<addressCount; i++) {
+                    try {
+                        netProp.addDnsServer(InetAddress.getByAddress(in.createByteArray()));
+                    } catch (UnknownHostException e) { }
+                }
+                netProp.setDomains(in.readString());
+                netProp.setMtu(in.readInt());
+                addressCount = in.readInt();
+                for (int i=0; i<addressCount; i++) {
+                    netProp.addRoute((RouteInfo)in.readParcelable(null));
+                }
+                if (in.readByte() == 1) {
+                    netProp.setHttpProxy((ProxyInfo)in.readParcelable(null));
+                }
+                ArrayList<LinkProperties> stackedLinks = new ArrayList<LinkProperties>();
+                in.readList(stackedLinks, LinkProperties.class.getClassLoader());
+                for (LinkProperties stackedLink: stackedLinks) {
+                    netProp.addStackedLink(stackedLink);
+                }
+                return netProp;
+            }
+
+            public LinkProperties[] newArray(int size) {
+                return new LinkProperties[size];
+            }
+        };
+}
diff --git a/core/java/android/net/Network.java b/core/java/android/net/Network.java
new file mode 100644
index 0000000..d933f26
--- /dev/null
+++ b/core/java/android/net/Network.java
@@ -0,0 +1,201 @@
+/*
+ * 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.NetworkUtils;
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.SocketFactory;
+
+/**
+ * Identifies a {@code Network}.  This is supplied to applications via
+ * {@link ConnectivityManager.NetworkCallbackListener} in response to
+ * {@link ConnectivityManager#requestNetwork} or {@link ConnectivityManager#listenForNetwork}.
+ * It is used to direct traffic to the given {@code Network}, either on a {@link Socket} basis
+ * through a targeted {@link SocketFactory} or process-wide via
+ * {@link ConnectivityManager#setProcessDefaultNetwork}.
+ */
+public class Network implements Parcelable {
+
+    /**
+     * @hide
+     */
+    public final int netId;
+
+    private NetworkBoundSocketFactory mNetworkBoundSocketFactory = null;
+
+    /**
+     * @hide
+     */
+    public Network(int netId) {
+        this.netId = netId;
+    }
+
+    /**
+     * @hide
+     */
+    public Network(Network that) {
+        this.netId = that.netId;
+    }
+
+    /**
+     * Operates the same as {@code InetAddress.getAllByName} except that host
+     * resolution is done on this network.
+     *
+     * @param host the hostname or literal IP string to be resolved.
+     * @return the array of addresses associated with the specified host.
+     * @throws UnknownHostException if the address lookup fails.
+     */
+    public InetAddress[] getAllByName(String host) throws UnknownHostException {
+        return InetAddress.getAllByNameOnNet(host, netId);
+    }
+
+    /**
+     * Operates the same as {@code InetAddress.getByName} except that host
+     * resolution is done on this network.
+     *
+     * @param host
+     *            the hostName to be resolved to an address or {@code null}.
+     * @return the {@code InetAddress} instance representing the host.
+     * @throws UnknownHostException
+     *             if the address lookup fails.
+     */
+    public InetAddress getByName(String host) throws UnknownHostException {
+        return InetAddress.getByNameOnNet(host, netId);
+    }
+
+    /**
+     * A {@code SocketFactory} that produces {@code Socket}'s bound to this network.
+     */
+    private class NetworkBoundSocketFactory extends SocketFactory {
+        private final int mNetId;
+
+        public NetworkBoundSocketFactory(int netId) {
+            super();
+            mNetId = netId;
+        }
+
+        private void connectToHost(Socket socket, String host, int port) throws IOException {
+            // Lookup addresses only on this Network.
+            InetAddress[] hostAddresses = getAllByName(host);
+            // Try all but last address ignoring exceptions.
+            for (int i = 0; i < hostAddresses.length - 1; i++) {
+                try {
+                    socket.connect(new InetSocketAddress(hostAddresses[i], port));
+                    return;
+                } catch (IOException e) {
+                }
+            }
+            // Try last address.  Do throw exceptions.
+            socket.connect(new InetSocketAddress(hostAddresses[hostAddresses.length - 1], port));
+        }
+
+        @Override
+        public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
+            Socket socket = createSocket();
+            socket.bind(new InetSocketAddress(localHost, localPort));
+            connectToHost(socket, host, port);
+            return socket;
+        }
+
+        @Override
+        public Socket createSocket(InetAddress address, int port, InetAddress localAddress,
+                int localPort) throws IOException {
+            Socket socket = createSocket();
+            socket.bind(new InetSocketAddress(localAddress, localPort));
+            socket.connect(new InetSocketAddress(address, port));
+            return socket;
+        }
+
+        @Override
+        public Socket createSocket(InetAddress host, int port) throws IOException {
+            Socket socket = createSocket();
+            socket.connect(new InetSocketAddress(host, port));
+            return socket;
+        }
+
+        @Override
+        public Socket createSocket(String host, int port) throws IOException {
+            Socket socket = createSocket();
+            connectToHost(socket, host, port);
+            return socket;
+        }
+
+        @Override
+        public Socket createSocket() throws IOException {
+            Socket socket = new Socket();
+            // Query a property of the underlying socket to ensure the underlying
+            // socket exists so a file descriptor is available to bind to a network.
+            socket.getReuseAddress();
+            NetworkUtils.bindSocketToNetwork(socket.getFileDescriptor$().getInt$(), mNetId);
+            return socket;
+        }
+    }
+
+    /**
+     * Returns a {@link SocketFactory} bound to this network.  Any {@link Socket} created by
+     * this factory will have its traffic sent over this {@code Network}.  Note that if this
+     * {@code Network} ever disconnects, this factory and any {@link Socket} it produced in the
+     * past or future will cease to work.
+     *
+     * @return a {@link SocketFactory} which produces {@link Socket} instances bound to this
+     *         {@code Network}.
+     */
+    public SocketFactory getSocketFactory() {
+        if (mNetworkBoundSocketFactory == null) {
+            mNetworkBoundSocketFactory = new NetworkBoundSocketFactory(netId);
+        }
+        return mNetworkBoundSocketFactory;
+    }
+
+    // implement the Parcelable interface
+    public int describeContents() {
+        return 0;
+    }
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(netId);
+    }
+
+    public static final Creator<Network> CREATOR =
+        new Creator<Network>() {
+            public Network createFromParcel(Parcel in) {
+                int netId = in.readInt();
+
+                return new Network(netId);
+            }
+
+            public Network[] newArray(int size) {
+                return new Network[size];
+            }
+    };
+
+    public boolean equals(Object obj) {
+        if (obj instanceof Network == false) return false;
+        Network other = (Network)obj;
+        return this.netId == other.netId;
+    }
+
+    public int hashCode() {
+        return netId * 11;
+    }
+}
diff --git a/core/java/android/net/NetworkAgent.java b/core/java/android/net/NetworkAgent.java
new file mode 100644
index 0000000..3d0874b
--- /dev/null
+++ b/core/java/android/net/NetworkAgent.java
@@ -0,0 +1,207 @@
+/*
+ * 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.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.Protocol;
+
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A Utility class for handling for communicating between bearer-specific
+ * code and ConnectivityService.
+ *
+ * A bearer may have more than one NetworkAgent if it can simultaneously
+ * support separate networks (IMS / Internet / MMS Apns on cellular, or
+ * perhaps connections with different SSID or P2P for Wi-Fi).
+ *
+ * @hide
+ */
+public abstract class NetworkAgent extends Handler {
+    private volatile AsyncChannel mAsyncChannel;
+    private final String LOG_TAG;
+    private static final boolean DBG = true;
+    private static final boolean VDBG = true;
+    private final Context mContext;
+    private final ArrayList<Message>mPreConnectedQueue = new ArrayList<Message>();
+
+    private static final int BASE = Protocol.BASE_NETWORK_AGENT;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to inform it of
+     * suspected connectivity problems on its network.  The NetworkAgent
+     * should take steps to verify and correct connectivity.
+     */
+    public static final int CMD_SUSPECT_BAD = BASE;
+
+    /**
+     * Sent by the NetworkAgent (note the EVENT vs CMD prefix) to
+     * ConnectivityService to pass the current NetworkInfo (connection state).
+     * Sent when the NetworkInfo changes, mainly due to change of state.
+     * obj = NetworkInfo
+     */
+    public static final int EVENT_NETWORK_INFO_CHANGED = BASE + 1;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the current
+     * NetworkCapabilties.
+     * obj = NetworkCapabilities
+     */
+    public static final int EVENT_NETWORK_CAPABILITIES_CHANGED = BASE + 2;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the current
+     * NetworkProperties.
+     * obj = NetworkProperties
+     */
+    public static final int EVENT_NETWORK_PROPERTIES_CHANGED = BASE + 3;
+
+    /* centralize place where base network score, and network score scaling, will be
+     * stored, so as we can consistently compare apple and oranges, or wifi, ethernet and LTE
+     */
+    public static final int WIFI_BASE_SCORE = 60;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the current
+     * network score.
+     * obj = network score Integer
+     */
+    public static final int EVENT_NETWORK_SCORE_CHANGED = BASE + 4;
+
+    public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni,
+            NetworkCapabilities nc, LinkProperties lp, int score) {
+        super(looper);
+        LOG_TAG = logTag;
+        mContext = context;
+        if (ni == null || nc == null || lp == null) {
+            throw new IllegalArgumentException();
+        }
+
+        if (DBG) log("Registering NetworkAgent");
+        ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService(
+                Context.CONNECTIVITY_SERVICE);
+        cm.registerNetworkAgent(new Messenger(this), new NetworkInfo(ni),
+                new LinkProperties(lp), new NetworkCapabilities(nc), score);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: {
+                if (mAsyncChannel != null) {
+                    log("Received new connection while already connected!");
+                } else {
+                    if (DBG) log("NetworkAgent fully connected");
+                    AsyncChannel ac = new AsyncChannel();
+                    ac.connected(null, this, msg.replyTo);
+                    ac.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
+                            AsyncChannel.STATUS_SUCCESSFUL);
+                    synchronized (mPreConnectedQueue) {
+                        mAsyncChannel = ac;
+                        for (Message m : mPreConnectedQueue) {
+                            ac.sendMessage(m);
+                        }
+                        mPreConnectedQueue.clear();
+                    }
+                }
+                break;
+            }
+            case AsyncChannel.CMD_CHANNEL_DISCONNECT: {
+                if (DBG) log("CMD_CHANNEL_DISCONNECT");
+                if (mAsyncChannel != null) mAsyncChannel.disconnect();
+                break;
+            }
+            case AsyncChannel.CMD_CHANNEL_DISCONNECTED: {
+                if (DBG) log("NetworkAgent channel lost");
+                // let the client know CS is done with us.
+                unwanted();
+                synchronized (mPreConnectedQueue) {
+                    mAsyncChannel = null;
+                }
+                break;
+            }
+            case CMD_SUSPECT_BAD: {
+                log("Unhandled Message " + msg);
+                break;
+            }
+        }
+    }
+
+    private void queueOrSendMessage(int what, Object obj) {
+        synchronized (mPreConnectedQueue) {
+            if (mAsyncChannel != null) {
+                mAsyncChannel.sendMessage(what, obj);
+            } else {
+                Message msg = Message.obtain();
+                msg.what = what;
+                msg.obj = obj;
+                mPreConnectedQueue.add(msg);
+            }
+        }
+    }
+
+    /**
+     * Called by the bearer code when it has new LinkProperties data.
+     */
+    public void sendLinkProperties(LinkProperties linkProperties) {
+        queueOrSendMessage(EVENT_NETWORK_PROPERTIES_CHANGED, new LinkProperties(linkProperties));
+    }
+
+    /**
+     * Called by the bearer code when it has new NetworkInfo data.
+     */
+    public void sendNetworkInfo(NetworkInfo networkInfo) {
+        queueOrSendMessage(EVENT_NETWORK_INFO_CHANGED, new NetworkInfo(networkInfo));
+    }
+
+    /**
+     * Called by the bearer code when it has new NetworkCapabilities data.
+     */
+    public void sendNetworkCapabilities(NetworkCapabilities networkCapabilities) {
+        queueOrSendMessage(EVENT_NETWORK_CAPABILITIES_CHANGED,
+                new NetworkCapabilities(networkCapabilities));
+    }
+
+    /**
+     * Called by the bearer code when it has a new score for this network.
+     */
+    public void sendNetworkScore(int score) {
+        queueOrSendMessage(EVENT_NETWORK_SCORE_CHANGED, new Integer(score));
+    }
+
+    /**
+     * Called when ConnectivityService has indicated they no longer want this network.
+     * The parent factory should (previously) have received indication of the change
+     * as well, either canceling NetworkRequests or altering their score such that this
+     * network won't be immediately requested again.
+     */
+    abstract protected void unwanted();
+
+    protected void log(String s) {
+        Log.d(LOG_TAG, "NetworkAgent: " + s);
+    }
+}
diff --git a/core/java/android/net/NetworkCapabilities.java b/core/java/android/net/NetworkCapabilities.java
new file mode 100644
index 0000000..fe96287
--- /dev/null
+++ b/core/java/android/net/NetworkCapabilities.java
@@ -0,0 +1,526 @@
+/*
+ * 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.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.lang.IllegalArgumentException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * This class represents the capabilities of a network.  This is used both to specify
+ * needs to {@link ConnectivityManager} and when inspecting a network.
+ *
+ * Note that this replaces the old {@link ConnectivityManager#TYPE_MOBILE} method
+ * of network selection.  Rather than indicate a need for Wi-Fi because an application
+ * needs high bandwidth and risk obselence when a new, fast network appears (like LTE),
+ * the application should specify it needs high bandwidth.  Similarly if an application
+ * needs an unmetered network for a bulk transfer it can specify that rather than assuming
+ * all cellular based connections are metered and all Wi-Fi based connections are not.
+ */
+public final class NetworkCapabilities implements Parcelable {
+    private static final String TAG = "NetworkCapabilities";
+    private static final boolean DBG = false;
+
+    /**
+     * @hide
+     */
+    public NetworkCapabilities() {
+    }
+
+    public NetworkCapabilities(NetworkCapabilities nc) {
+        if (nc != null) {
+            mNetworkCapabilities = nc.mNetworkCapabilities;
+            mTransportTypes = nc.mTransportTypes;
+            mLinkUpBandwidthKbps = nc.mLinkUpBandwidthKbps;
+            mLinkDownBandwidthKbps = nc.mLinkDownBandwidthKbps;
+        }
+    }
+
+    /**
+     * Represents the network's capabilities.  If any are specified they will be satisfied
+     * by any Network that matches all of them.
+     */
+    private long mNetworkCapabilities = (1 << NET_CAPABILITY_NOT_RESTRICTED);
+
+    /**
+     * Indicates this is a network that has the ability to reach the
+     * carrier's MMSC for sending and receiving MMS messages.
+     */
+    public static final int NET_CAPABILITY_MMS            = 0;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * SUPL server, used to retrieve GPS information.
+     */
+    public static final int NET_CAPABILITY_SUPL           = 1;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * DUN or tethering gateway.
+     */
+    public static final int NET_CAPABILITY_DUN            = 2;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * FOTA portal, used for over the air updates.
+     */
+    public static final int NET_CAPABILITY_FOTA           = 3;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * IMS servers, used for network registration and signaling.
+     */
+    public static final int NET_CAPABILITY_IMS            = 4;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * CBS servers, used for carrier specific services.
+     */
+    public static final int NET_CAPABILITY_CBS            = 5;
+
+    /**
+     * Indicates this is a network that has the ability to reach a Wi-Fi direct
+     * peer.
+     */
+    public static final int NET_CAPABILITY_WIFI_P2P       = 6;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's
+     * Initial Attach servers.
+     */
+    public static final int NET_CAPABILITY_IA             = 7;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's
+     * RCS servers, used for Rich Communication Services.
+     */
+    public static final int NET_CAPABILITY_RCS            = 8;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's
+     * XCAP servers, used for configuration and control.
+     */
+    public static final int NET_CAPABILITY_XCAP           = 9;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's
+     * Emergency IMS servers, used for network signaling during emergency calls.
+     */
+    public static final int NET_CAPABILITY_EIMS           = 10;
+
+    /**
+     * Indicates that this network is unmetered.
+     */
+    public static final int NET_CAPABILITY_NOT_METERED    = 11;
+
+    /**
+     * Indicates that this network should be able to reach the internet.
+     */
+    public static final int NET_CAPABILITY_INTERNET       = 12;
+
+    /**
+     * Indicates that this network is available for general use.  If this is not set
+     * applications should not attempt to communicate on this network.  Note that this
+     * is simply informative and not enforcement - enforcement is handled via other means.
+     * Set by default.
+     */
+    public static final int NET_CAPABILITY_NOT_RESTRICTED = 13;
+
+    private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS;
+    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_RESTRICTED;
+
+    /**
+     * Adds the given capability to this {@code NetworkCapability} instance.
+     * Multiple capabilities may be applied sequentially.  Note that when searching
+     * for a network to satisfy a request, all capabilities requested must be satisfied.
+     *
+     * @param capability the {@code NetworkCapabilities.NET_CAPABILITY_*} to be added.
+     * @return This NetworkCapability to facilitate chaining.
+     * @hide
+     */
+    public NetworkCapabilities addCapability(int capability) {
+        if (capability < MIN_NET_CAPABILITY || capability > MAX_NET_CAPABILITY) {
+            throw new IllegalArgumentException("NetworkCapability out of range");
+        }
+        mNetworkCapabilities |= 1 << capability;
+        return this;
+    }
+
+    /**
+     * Removes (if found) the given capability from this {@code NetworkCapability} instance.
+     *
+     * @param capability the {@code NetworkCapabilities.NET_CAPABILTIY_*} to be removed.
+     * @return This NetworkCapability to facilitate chaining.
+     * @hide
+     */
+    public NetworkCapabilities removeCapability(int capability) {
+        if (capability < MIN_NET_CAPABILITY || capability > MAX_NET_CAPABILITY) {
+            throw new IllegalArgumentException("NetworkCapability out of range");
+        }
+        mNetworkCapabilities &= ~(1 << capability);
+        return this;
+    }
+
+    /**
+     * Gets all the capabilities set on this {@code NetworkCapability} instance.
+     *
+     * @return an array of {@code NetworkCapabilities.NET_CAPABILITY_*} values
+     *         for this instance.
+     * @hide
+     */
+    public int[] getCapabilities() {
+        return enumerateBits(mNetworkCapabilities);
+    }
+
+    /**
+     * Tests for the presence of a capabilitity on this instance.
+     *
+     * @param capability the {@code NetworkCapabilities.NET_CAPABILITY_*} to be tested for.
+     * @return {@code true} if set on this instance.
+     */
+    public boolean hasCapability(int capability) {
+        if (capability < MIN_NET_CAPABILITY || capability > MAX_NET_CAPABILITY) {
+            return false;
+        }
+        return ((mNetworkCapabilities & (1 << capability)) != 0);
+    }
+
+    private int[] enumerateBits(long val) {
+        int size = Long.bitCount(val);
+        int[] result = new int[size];
+        int index = 0;
+        int resource = 0;
+        while (val > 0) {
+            if ((val & 1) == 1) result[index++] = resource;
+            val = val >> 1;
+            resource++;
+        }
+        return result;
+    }
+
+    private void combineNetCapabilities(NetworkCapabilities nc) {
+        this.mNetworkCapabilities |= nc.mNetworkCapabilities;
+    }
+
+    private boolean satisfiedByNetCapabilities(NetworkCapabilities nc) {
+        return ((nc.mNetworkCapabilities & this.mNetworkCapabilities) == this.mNetworkCapabilities);
+    }
+
+    private boolean equalsNetCapabilities(NetworkCapabilities nc) {
+        return (nc.mNetworkCapabilities == this.mNetworkCapabilities);
+    }
+
+    /**
+     * Representing the transport type.  Apps should generally not care about transport.  A
+     * request for a fast internet connection could be satisfied by a number of different
+     * transports.  If any are specified here it will be satisfied a Network that matches
+     * any of them.  If a caller doesn't care about the transport it should not specify any.
+     */
+    private long mTransportTypes;
+
+    /**
+     * Indicates this network uses a Cellular transport.
+     */
+    public static final int TRANSPORT_CELLULAR = 0;
+
+    /**
+     * Indicates this network uses a Wi-Fi transport.
+     */
+    public static final int TRANSPORT_WIFI = 1;
+
+    /**
+     * Indicates this network uses a Bluetooth transport.
+     */
+    public static final int TRANSPORT_BLUETOOTH = 2;
+
+    /**
+     * Indicates this network uses an Ethernet transport.
+     */
+    public static final int TRANSPORT_ETHERNET = 3;
+
+    private static final int MIN_TRANSPORT = TRANSPORT_CELLULAR;
+    private static final int MAX_TRANSPORT = TRANSPORT_ETHERNET;
+
+    /**
+     * Adds the given transport type to this {@code NetworkCapability} instance.
+     * Multiple transports may be applied sequentially.  Note that when searching
+     * for a network to satisfy a request, any listed in the request will satisfy the request.
+     * For example {@code TRANSPORT_WIFI} and {@code TRANSPORT_ETHERNET} added to a
+     * {@code NetworkCapabilities} would cause either a Wi-Fi network or an Ethernet network
+     * to be selected.  This is logically different than
+     * {@code NetworkCapabilities.NET_CAPABILITY_*} listed above.
+     *
+     * @param transportType the {@code NetworkCapabilities.TRANSPORT_*} to be added.
+     * @return This NetworkCapability to facilitate chaining.
+     * @hide
+     */
+    public NetworkCapabilities addTransportType(int transportType) {
+        if (transportType < MIN_TRANSPORT || transportType > MAX_TRANSPORT) {
+            throw new IllegalArgumentException("TransportType out of range");
+        }
+        mTransportTypes |= 1 << transportType;
+        return this;
+    }
+
+    /**
+     * Removes (if found) the given transport from this {@code NetworkCapability} instance.
+     *
+     * @param transportType the {@code NetworkCapabilities.TRANSPORT_*} to be removed.
+     * @return This NetworkCapability to facilitate chaining.
+     * @hide
+     */
+    public NetworkCapabilities removeTransportType(int transportType) {
+        if (transportType < MIN_TRANSPORT || transportType > MAX_TRANSPORT) {
+            throw new IllegalArgumentException("TransportType out of range");
+        }
+        mTransportTypes &= ~(1 << transportType);
+        return this;
+    }
+
+    /**
+     * Gets all the transports set on this {@code NetworkCapability} instance.
+     *
+     * @return an array of {@code NetworkCapabilities.TRANSPORT_*} values
+     *         for this instance.
+     * @hide
+     */
+    public int[] getTransportTypes() {
+        return enumerateBits(mTransportTypes);
+    }
+
+    /**
+     * Tests for the presence of a transport on this instance.
+     *
+     * @param transportType the {@code NetworkCapabilities.TRANSPORT_*} to be tested for.
+     * @return {@code true} if set on this instance.
+     */
+    public boolean hasTransport(int transportType) {
+        if (transportType < MIN_TRANSPORT || transportType > MAX_TRANSPORT) {
+            return false;
+        }
+        return ((mTransportTypes & (1 << transportType)) != 0);
+    }
+
+    private void combineTransportTypes(NetworkCapabilities nc) {
+        this.mTransportTypes |= nc.mTransportTypes;
+    }
+    private boolean satisfiedByTransportTypes(NetworkCapabilities nc) {
+        return ((this.mTransportTypes == 0) ||
+                ((this.mTransportTypes & nc.mTransportTypes) != 0));
+    }
+    private boolean equalsTransportTypes(NetworkCapabilities nc) {
+        return (nc.mTransportTypes == this.mTransportTypes);
+    }
+
+    /**
+     * Passive link bandwidth.  This is a rough guide of the expected peak bandwidth
+     * for the first hop on the given transport.  It is not measured, but may take into account
+     * link parameters (Radio technology, allocated channels, etc).
+     */
+    private int mLinkUpBandwidthKbps;
+    private int mLinkDownBandwidthKbps;
+
+    /**
+     * Sets the upstream bandwidth for this network in Kbps.  This always only refers to
+     * the estimated first hop transport bandwidth.
+     * <p>
+     * Note that when used to request a network, this specifies the minimum acceptable.
+     * When received as the state of an existing network this specifies the typical
+     * first hop bandwidth expected.  This is never measured, but rather is inferred
+     * from technology type and other link parameters.  It could be used to differentiate
+     * between very slow 1xRTT cellular links and other faster networks or even between
+     * 802.11b vs 802.11AC wifi technologies.  It should not be used to differentiate between
+     * fast backhauls and slow backhauls.
+     *
+     * @param upKbps the estimated first hop upstream (device to network) bandwidth.
+     * @hide
+     */
+    public void setLinkUpstreamBandwidthKbps(int upKbps) {
+        mLinkUpBandwidthKbps = upKbps;
+    }
+
+    /**
+     * Retrieves the upstream bandwidth for this network in Kbps.  This always only refers to
+     * the estimated first hop transport bandwidth.
+     *
+     * @return The estimated first hop upstream (device to network) bandwidth.
+     */
+    public int getLinkUpstreamBandwidthKbps() {
+        return mLinkUpBandwidthKbps;
+    }
+
+    /**
+     * Sets the downstream bandwidth for this network in Kbps.  This always only refers to
+     * the estimated first hop transport bandwidth.
+     * <p>
+     * Note that when used to request a network, this specifies the minimum acceptable.
+     * When received as the state of an existing network this specifies the typical
+     * first hop bandwidth expected.  This is never measured, but rather is inferred
+     * from technology type and other link parameters.  It could be used to differentiate
+     * between very slow 1xRTT cellular links and other faster networks or even between
+     * 802.11b vs 802.11AC wifi technologies.  It should not be used to differentiate between
+     * fast backhauls and slow backhauls.
+     *
+     * @param downKbps the estimated first hop downstream (network to device) bandwidth.
+     * @hide
+     */
+    public void setLinkDownstreamBandwidthKbps(int downKbps) {
+        mLinkDownBandwidthKbps = downKbps;
+    }
+
+    /**
+     * Retrieves the downstream bandwidth for this network in Kbps.  This always only refers to
+     * the estimated first hop transport bandwidth.
+     *
+     * @return The estimated first hop downstream (network to device) bandwidth.
+     */
+    public int getLinkDownstreamBandwidthKbps() {
+        return mLinkDownBandwidthKbps;
+    }
+
+    private void combineLinkBandwidths(NetworkCapabilities nc) {
+        this.mLinkUpBandwidthKbps =
+                Math.max(this.mLinkUpBandwidthKbps, nc.mLinkUpBandwidthKbps);
+        this.mLinkDownBandwidthKbps =
+                Math.max(this.mLinkDownBandwidthKbps, nc.mLinkDownBandwidthKbps);
+    }
+    private boolean satisfiedByLinkBandwidths(NetworkCapabilities nc) {
+        return !(this.mLinkUpBandwidthKbps > nc.mLinkUpBandwidthKbps ||
+                this.mLinkDownBandwidthKbps > nc.mLinkDownBandwidthKbps);
+    }
+    private boolean equalsLinkBandwidths(NetworkCapabilities nc) {
+        return (this.mLinkUpBandwidthKbps == nc.mLinkUpBandwidthKbps &&
+                this.mLinkDownBandwidthKbps == nc.mLinkDownBandwidthKbps);
+    }
+
+    /**
+     * Combine a set of Capabilities to this one.  Useful for coming up with the complete set
+     * {@hide}
+     */
+    public void combineCapabilities(NetworkCapabilities nc) {
+        combineNetCapabilities(nc);
+        combineTransportTypes(nc);
+        combineLinkBandwidths(nc);
+    }
+
+    /**
+     * Check if our requirements are satisfied by the given Capabilities.
+     * {@hide}
+     */
+    public boolean satisfiedByNetworkCapabilities(NetworkCapabilities nc) {
+        return (nc != null &&
+                satisfiedByNetCapabilities(nc) &&
+                satisfiedByTransportTypes(nc) &&
+                satisfiedByLinkBandwidths(nc));
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || (obj instanceof NetworkCapabilities == false)) return false;
+        NetworkCapabilities that = (NetworkCapabilities)obj;
+        return (equalsNetCapabilities(that) &&
+                equalsTransportTypes(that) &&
+                equalsLinkBandwidths(that));
+    }
+
+    @Override
+    public int hashCode() {
+        return ((int)(mNetworkCapabilities & 0xFFFFFFFF) +
+                ((int)(mNetworkCapabilities >> 32) * 3) +
+                ((int)(mTransportTypes & 0xFFFFFFFF) * 5) +
+                ((int)(mTransportTypes >> 32) * 7) +
+                (mLinkUpBandwidthKbps * 11) +
+                (mLinkDownBandwidthKbps * 13));
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeLong(mNetworkCapabilities);
+        dest.writeLong(mTransportTypes);
+        dest.writeInt(mLinkUpBandwidthKbps);
+        dest.writeInt(mLinkDownBandwidthKbps);
+    }
+    public static final Creator<NetworkCapabilities> CREATOR =
+        new Creator<NetworkCapabilities>() {
+            public NetworkCapabilities createFromParcel(Parcel in) {
+                NetworkCapabilities netCap = new NetworkCapabilities();
+
+                netCap.mNetworkCapabilities = in.readLong();
+                netCap.mTransportTypes = in.readLong();
+                netCap.mLinkUpBandwidthKbps = in.readInt();
+                netCap.mLinkDownBandwidthKbps = in.readInt();
+                return netCap;
+            }
+            public NetworkCapabilities[] newArray(int size) {
+                return new NetworkCapabilities[size];
+            }
+        };
+
+    public String toString() {
+        int[] types = getTransportTypes();
+        String transports = (types.length > 0 ? " Transports: " : "");
+        for (int i = 0; i < types.length;) {
+            switch (types[i]) {
+                case TRANSPORT_CELLULAR:    transports += "CELLULAR"; break;
+                case TRANSPORT_WIFI:        transports += "WIFI"; break;
+                case TRANSPORT_BLUETOOTH:   transports += "BLUETOOTH"; break;
+                case TRANSPORT_ETHERNET:    transports += "ETHERNET"; break;
+            }
+            if (++i < types.length) transports += "|";
+        }
+
+        types = getCapabilities();
+        String capabilities = (types.length > 0 ? " Capabilities: " : "");
+        for (int i = 0; i < types.length; ) {
+            switch (types[i]) {
+                case NET_CAPABILITY_MMS:            capabilities += "MMS"; break;
+                case NET_CAPABILITY_SUPL:           capabilities += "SUPL"; break;
+                case NET_CAPABILITY_DUN:            capabilities += "DUN"; break;
+                case NET_CAPABILITY_FOTA:           capabilities += "FOTA"; break;
+                case NET_CAPABILITY_IMS:            capabilities += "IMS"; break;
+                case NET_CAPABILITY_CBS:            capabilities += "CBS"; break;
+                case NET_CAPABILITY_WIFI_P2P:       capabilities += "WIFI_P2P"; break;
+                case NET_CAPABILITY_IA:             capabilities += "IA"; break;
+                case NET_CAPABILITY_RCS:            capabilities += "RCS"; break;
+                case NET_CAPABILITY_XCAP:           capabilities += "XCAP"; break;
+                case NET_CAPABILITY_EIMS:           capabilities += "EIMS"; break;
+                case NET_CAPABILITY_NOT_METERED:    capabilities += "NOT_METERED"; break;
+                case NET_CAPABILITY_INTERNET:       capabilities += "INTERNET"; break;
+                case NET_CAPABILITY_NOT_RESTRICTED: capabilities += "NOT_RESTRICTED"; break;
+            }
+            if (++i < types.length) capabilities += "&";
+        }
+
+        String upBand = ((mLinkUpBandwidthKbps > 0) ? " LinkUpBandwidth>=" +
+                mLinkUpBandwidthKbps + "Kbps" : "");
+        String dnBand = ((mLinkDownBandwidthKbps > 0) ? " LinkDnBandwidth>=" +
+                mLinkDownBandwidthKbps + "Kbps" : "");
+
+        return "[" + transports + capabilities + upBand + dnBand + "]";
+    }
+}
diff --git a/core/java/android/net/NetworkConfig.java b/core/java/android/net/NetworkConfig.java
new file mode 100644
index 0000000..32a2cda
--- /dev/null
+++ b/core/java/android/net/NetworkConfig.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+import java.util.Locale;
+
+/**
+ * Describes the buildtime configuration of a network.
+ * Holds settings read from resources.
+ * @hide
+ */
+public class NetworkConfig {
+    /**
+     * Human readable string
+     */
+    public String name;
+
+    /**
+     * Type from ConnectivityManager
+     */
+    public int type;
+
+    /**
+     * the radio number from radio attributes config
+     */
+    public int radio;
+
+    /**
+     * higher number == higher priority when turning off connections
+     */
+    public int priority;
+
+    /**
+     * indicates the boot time dependencyMet setting
+     */
+    public boolean dependencyMet;
+
+    /**
+     * indicates the default restoral timer in seconds
+     * if the network is used as a special network feature
+     * -1 indicates no restoration of default
+     */
+    public int restoreTime;
+
+    /**
+     * input string from config.xml resource.  Uses the form:
+     * [Connection name],[ConnectivityManager connection type],
+     * [associated radio-type],[priority],[dependencyMet]
+     */
+    public NetworkConfig(String init) {
+        String fragments[] = init.split(",");
+        name = fragments[0].trim().toLowerCase(Locale.ROOT);
+        type = Integer.parseInt(fragments[1]);
+        radio = Integer.parseInt(fragments[2]);
+        priority = Integer.parseInt(fragments[3]);
+        restoreTime = Integer.parseInt(fragments[4]);
+        dependencyMet = Boolean.parseBoolean(fragments[5]);
+    }
+
+    /**
+     * Indicates if this network is supposed to be default-routable
+     */
+    public boolean isDefault() {
+        return (type == radio);
+    }
+}
diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java
new file mode 100644
index 0000000..d279412
--- /dev/null
+++ b/core/java/android/net/NetworkInfo.java
@@ -0,0 +1,506 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Parcelable;
+import android.os.Parcel;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.EnumMap;
+
+/**
+ * Describes the status of a network interface.
+ * <p>Use {@link ConnectivityManager#getActiveNetworkInfo()} to get an instance that represents
+ * the current network connection.
+ */
+public class NetworkInfo implements Parcelable {
+
+    /**
+     * Coarse-grained network state. This is probably what most applications should
+     * use, rather than {@link android.net.NetworkInfo.DetailedState DetailedState}.
+     * The mapping between the two is as follows:
+     * <br/><br/>
+     * <table>
+     * <tr><td><b>Detailed state</b></td><td><b>Coarse-grained state</b></td></tr>
+     * <tr><td><code>IDLE</code></td><td><code>DISCONNECTED</code></td></tr>
+     * <tr><td><code>SCANNING</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>CONNECTING</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>AUTHENTICATING</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>CONNECTED</code></td><td><code>CONNECTED</code></td></tr>
+     * <tr><td><code>DISCONNECTING</code></td><td><code>DISCONNECTING</code></td></tr>
+     * <tr><td><code>DISCONNECTED</code></td><td><code>DISCONNECTED</code></td></tr>
+     * <tr><td><code>UNAVAILABLE</code></td><td><code>DISCONNECTED</code></td></tr>
+     * <tr><td><code>FAILED</code></td><td><code>DISCONNECTED</code></td></tr>
+     * </table>
+     */
+    public enum State {
+        CONNECTING, CONNECTED, SUSPENDED, DISCONNECTING, DISCONNECTED, UNKNOWN
+    }
+
+    /**
+     * The fine-grained state of a network connection. This level of detail
+     * is probably of interest to few applications. Most should use
+     * {@link android.net.NetworkInfo.State State} instead.
+     */
+    public enum DetailedState {
+        /** Ready to start data connection setup. */
+        IDLE,
+        /** Searching for an available access point. */
+        SCANNING,
+        /** Currently setting up data connection. */
+        CONNECTING,
+        /** Network link established, performing authentication. */
+        AUTHENTICATING,
+        /** Awaiting response from DHCP server in order to assign IP address information. */
+        OBTAINING_IPADDR,
+        /** IP traffic should be available. */
+        CONNECTED,
+        /** IP traffic is suspended */
+        SUSPENDED,
+        /** Currently tearing down data connection. */
+        DISCONNECTING,
+        /** IP traffic not available. */
+        DISCONNECTED,
+        /** Attempt to connect failed. */
+        FAILED,
+        /** Access to this network is blocked. */
+        BLOCKED,
+        /** Link has poor connectivity. */
+        VERIFYING_POOR_LINK,
+        /** Checking if network is a captive portal */
+        CAPTIVE_PORTAL_CHECK
+    }
+
+    /**
+     * This is the map described in the Javadoc comment above. The positions
+     * of the elements of the array must correspond to the ordinal values
+     * of <code>DetailedState</code>.
+     */
+    private static final EnumMap<DetailedState, State> stateMap =
+        new EnumMap<DetailedState, State>(DetailedState.class);
+
+    static {
+        stateMap.put(DetailedState.IDLE, State.DISCONNECTED);
+        stateMap.put(DetailedState.SCANNING, State.DISCONNECTED);
+        stateMap.put(DetailedState.CONNECTING, State.CONNECTING);
+        stateMap.put(DetailedState.AUTHENTICATING, State.CONNECTING);
+        stateMap.put(DetailedState.OBTAINING_IPADDR, State.CONNECTING);
+        stateMap.put(DetailedState.VERIFYING_POOR_LINK, State.CONNECTING);
+        stateMap.put(DetailedState.CAPTIVE_PORTAL_CHECK, State.CONNECTING);
+        stateMap.put(DetailedState.CONNECTED, State.CONNECTED);
+        stateMap.put(DetailedState.SUSPENDED, State.SUSPENDED);
+        stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING);
+        stateMap.put(DetailedState.DISCONNECTED, State.DISCONNECTED);
+        stateMap.put(DetailedState.FAILED, State.DISCONNECTED);
+        stateMap.put(DetailedState.BLOCKED, State.DISCONNECTED);
+    }
+
+    private int mNetworkType;
+    private int mSubtype;
+    private String mTypeName;
+    private String mSubtypeName;
+    private State mState;
+    private DetailedState mDetailedState;
+    private String mReason;
+    private String mExtraInfo;
+    private boolean mIsFailover;
+    private boolean mIsRoaming;
+    private boolean mIsConnectedToProvisioningNetwork;
+
+    /**
+     * Indicates whether network connectivity is possible:
+     */
+    private boolean mIsAvailable;
+
+    /**
+     * @param type network type
+     * @deprecated
+     * @hide because this constructor was only meant for internal use (and
+     * has now been superseded by the package-private constructor below).
+     */
+    public NetworkInfo(int type) {}
+
+    /**
+     * @hide
+     */
+    public NetworkInfo(int type, int subtype, String typeName, String subtypeName) {
+        if (!ConnectivityManager.isNetworkTypeValid(type)) {
+            throw new IllegalArgumentException("Invalid network type: " + type);
+        }
+        mNetworkType = type;
+        mSubtype = subtype;
+        mTypeName = typeName;
+        mSubtypeName = subtypeName;
+        setDetailedState(DetailedState.IDLE, null, null);
+        mState = State.UNKNOWN;
+        mIsAvailable = false; // until we're told otherwise, assume unavailable
+        mIsRoaming = false;
+        mIsConnectedToProvisioningNetwork = false;
+    }
+
+    /** {@hide} */
+    public NetworkInfo(NetworkInfo source) {
+        if (source != null) {
+            synchronized (source) {
+                mNetworkType = source.mNetworkType;
+                mSubtype = source.mSubtype;
+                mTypeName = source.mTypeName;
+                mSubtypeName = source.mSubtypeName;
+                mState = source.mState;
+                mDetailedState = source.mDetailedState;
+                mReason = source.mReason;
+                mExtraInfo = source.mExtraInfo;
+                mIsFailover = source.mIsFailover;
+                mIsRoaming = source.mIsRoaming;
+                mIsAvailable = source.mIsAvailable;
+                mIsConnectedToProvisioningNetwork = source.mIsConnectedToProvisioningNetwork;
+            }
+        }
+    }
+
+    /**
+     * Reports the type of network to which the
+     * info in this {@code NetworkInfo} pertains.
+     * @return one of {@link ConnectivityManager#TYPE_MOBILE}, {@link
+     * ConnectivityManager#TYPE_WIFI}, {@link ConnectivityManager#TYPE_WIMAX}, {@link
+     * ConnectivityManager#TYPE_ETHERNET},  {@link ConnectivityManager#TYPE_BLUETOOTH}, or other
+     * types defined by {@link ConnectivityManager}
+     */
+    public int getType() {
+        synchronized (this) {
+            return mNetworkType;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public void setType(int type) {
+        synchronized (this) {
+            mNetworkType = type;
+        }
+    }
+
+    /**
+     * Return a network-type-specific integer describing the subtype
+     * of the network.
+     * @return the network subtype
+     */
+    public int getSubtype() {
+        synchronized (this) {
+            return mSubtype;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public void setSubtype(int subtype, String subtypeName) {
+        synchronized (this) {
+            mSubtype = subtype;
+            mSubtypeName = subtypeName;
+        }
+    }
+
+    /**
+     * Return a human-readable name describe the type of the network,
+     * for example "WIFI" or "MOBILE".
+     * @return the name of the network type
+     */
+    public String getTypeName() {
+        synchronized (this) {
+            return mTypeName;
+        }
+    }
+
+    /**
+     * Return a human-readable name describing the subtype of the network.
+     * @return the name of the network subtype
+     */
+    public String getSubtypeName() {
+        synchronized (this) {
+            return mSubtypeName;
+        }
+    }
+
+    /**
+     * Indicates whether network connectivity exists or is in the process
+     * of being established. This is good for applications that need to
+     * do anything related to the network other than read or write data.
+     * For the latter, call {@link #isConnected()} instead, which guarantees
+     * that the network is fully usable.
+     * @return {@code true} if network connectivity exists or is in the process
+     * of being established, {@code false} otherwise.
+     */
+    public boolean isConnectedOrConnecting() {
+        synchronized (this) {
+            return mState == State.CONNECTED || mState == State.CONNECTING;
+        }
+    }
+
+    /**
+     * Indicates whether network connectivity exists and it is possible to establish
+     * connections and pass data.
+     * <p>Always call this before attempting to perform data transactions.
+     * @return {@code true} if network connectivity exists, {@code false} otherwise.
+     */
+    public boolean isConnected() {
+        synchronized (this) {
+            return mState == State.CONNECTED;
+        }
+    }
+
+    /**
+     * Indicates whether network connectivity is possible. A network is unavailable
+     * when a persistent or semi-persistent condition prevents the possibility
+     * of connecting to that network. Examples include
+     * <ul>
+     * <li>The device is out of the coverage area for any network of this type.</li>
+     * <li>The device is on a network other than the home network (i.e., roaming), and
+     * data roaming has been disabled.</li>
+     * <li>The device's radio is turned off, e.g., because airplane mode is enabled.</li>
+     * </ul>
+     * @return {@code true} if the network is available, {@code false} otherwise
+     */
+    public boolean isAvailable() {
+        synchronized (this) {
+            return mIsAvailable;
+        }
+    }
+
+    /**
+     * Sets if the network is available, ie, if the connectivity is possible.
+     * @param isAvailable the new availability value.
+     *
+     * @hide
+     */
+    public void setIsAvailable(boolean isAvailable) {
+        synchronized (this) {
+            mIsAvailable = isAvailable;
+        }
+    }
+
+    /**
+     * Indicates whether the current attempt to connect to the network
+     * resulted from the ConnectivityManager trying to fail over to this
+     * network following a disconnect from another network.
+     * @return {@code true} if this is a failover attempt, {@code false}
+     * otherwise.
+     */
+    public boolean isFailover() {
+        synchronized (this) {
+            return mIsFailover;
+        }
+    }
+
+    /**
+     * Set the failover boolean.
+     * @param isFailover {@code true} to mark the current connection attempt
+     * as a failover.
+     * @hide
+     */
+    public void setFailover(boolean isFailover) {
+        synchronized (this) {
+            mIsFailover = isFailover;
+        }
+    }
+
+    /**
+     * Indicates whether the device is currently roaming on this network.
+     * When {@code true}, it suggests that use of data on this network
+     * may incur extra costs.
+     * @return {@code true} if roaming is in effect, {@code false} otherwise.
+     */
+    public boolean isRoaming() {
+        synchronized (this) {
+            return mIsRoaming;
+        }
+    }
+
+    /** {@hide} */
+    @VisibleForTesting
+    public void setRoaming(boolean isRoaming) {
+        synchronized (this) {
+            mIsRoaming = isRoaming;
+        }
+    }
+
+    /** {@hide} */
+    @VisibleForTesting
+    public boolean isConnectedToProvisioningNetwork() {
+        synchronized (this) {
+            return mIsConnectedToProvisioningNetwork;
+        }
+    }
+
+    /** {@hide} */
+    @VisibleForTesting
+    public void setIsConnectedToProvisioningNetwork(boolean val) {
+        synchronized (this) {
+            mIsConnectedToProvisioningNetwork = val;
+        }
+    }
+
+    /**
+     * Reports the current coarse-grained state of the network.
+     * @return the coarse-grained state
+     */
+    public State getState() {
+        synchronized (this) {
+            return mState;
+        }
+    }
+
+    /**
+     * Reports the current fine-grained state of the network.
+     * @return the fine-grained state
+     */
+    public DetailedState getDetailedState() {
+        synchronized (this) {
+            return mDetailedState;
+        }
+    }
+
+    /**
+     * Sets the fine-grained state of the network.
+     * @param detailedState the {@link DetailedState}.
+     * @param reason a {@code String} indicating the reason for the state change,
+     * if one was supplied. May be {@code null}.
+     * @param extraInfo an optional {@code String} providing addditional network state
+     * information passed up from the lower networking layers.
+     * @hide
+     */
+    public void setDetailedState(DetailedState detailedState, String reason, String extraInfo) {
+        synchronized (this) {
+            this.mDetailedState = detailedState;
+            this.mState = stateMap.get(detailedState);
+            this.mReason = reason;
+            this.mExtraInfo = extraInfo;
+        }
+    }
+
+    /**
+     * Set the extraInfo field.
+     * @param extraInfo an optional {@code String} providing addditional network state
+     * information passed up from the lower networking layers.
+     * @hide
+     */
+    public void setExtraInfo(String extraInfo) {
+        synchronized (this) {
+            this.mExtraInfo = extraInfo;
+        }
+    }
+
+    /**
+     * Report the reason an attempt to establish connectivity failed,
+     * if one is available.
+     * @return the reason for failure, or null if not available
+     */
+    public String getReason() {
+        synchronized (this) {
+            return mReason;
+        }
+    }
+
+    /**
+     * Report the extra information about the network state, if any was
+     * provided by the lower networking layers.,
+     * if one is available.
+     * @return the extra information, or null if not available
+     */
+    public String getExtraInfo() {
+        synchronized (this) {
+            return mExtraInfo;
+        }
+    }
+
+    @Override
+    public String toString() {
+        synchronized (this) {
+            StringBuilder builder = new StringBuilder("[");
+            builder.append("type: ").append(getTypeName()).append("[").append(getSubtypeName()).
+            append("], state: ").append(mState).append("/").append(mDetailedState).
+            append(", reason: ").append(mReason == null ? "(unspecified)" : mReason).
+            append(", extra: ").append(mExtraInfo == null ? "(none)" : mExtraInfo).
+            append(", roaming: ").append(mIsRoaming).
+            append(", failover: ").append(mIsFailover).
+            append(", isAvailable: ").append(mIsAvailable).
+            append(", isConnectedToProvisioningNetwork: ").
+            append(mIsConnectedToProvisioningNetwork).
+            append("]");
+            return builder.toString();
+        }
+    }
+
+    /**
+     * Implement the Parcelable interface
+     * @hide
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        synchronized (this) {
+            dest.writeInt(mNetworkType);
+            dest.writeInt(mSubtype);
+            dest.writeString(mTypeName);
+            dest.writeString(mSubtypeName);
+            dest.writeString(mState.name());
+            dest.writeString(mDetailedState.name());
+            dest.writeInt(mIsFailover ? 1 : 0);
+            dest.writeInt(mIsAvailable ? 1 : 0);
+            dest.writeInt(mIsRoaming ? 1 : 0);
+            dest.writeInt(mIsConnectedToProvisioningNetwork ? 1 : 0);
+            dest.writeString(mReason);
+            dest.writeString(mExtraInfo);
+        }
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public static final Creator<NetworkInfo> CREATOR =
+        new Creator<NetworkInfo>() {
+            public NetworkInfo createFromParcel(Parcel in) {
+                int netType = in.readInt();
+                int subtype = in.readInt();
+                String typeName = in.readString();
+                String subtypeName = in.readString();
+                NetworkInfo netInfo = new NetworkInfo(netType, subtype, typeName, subtypeName);
+                netInfo.mState = State.valueOf(in.readString());
+                netInfo.mDetailedState = DetailedState.valueOf(in.readString());
+                netInfo.mIsFailover = in.readInt() != 0;
+                netInfo.mIsAvailable = in.readInt() != 0;
+                netInfo.mIsRoaming = in.readInt() != 0;
+                netInfo.mIsConnectedToProvisioningNetwork = in.readInt() != 0;
+                netInfo.mReason = in.readString();
+                netInfo.mExtraInfo = in.readString();
+                return netInfo;
+            }
+
+            public NetworkInfo[] newArray(int size) {
+                return new NetworkInfo[size];
+            }
+        };
+}
diff --git a/core/java/android/net/NetworkRequest.java b/core/java/android/net/NetworkRequest.java
new file mode 100644
index 0000000..7911c72
--- /dev/null
+++ b/core/java/android/net/NetworkRequest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Defines a request for a network, made through {@link NetworkRequest.Builder} and used
+ * to request a network via {@link ConnectivityManager#requestNetwork} or listen for changes
+ * via {@link ConnectivityManager#listenForNetwork}.
+ */
+public class NetworkRequest implements Parcelable {
+    /**
+     * The {@link NetworkCapabilities} that define this request.
+     * @hide
+     */
+    public final NetworkCapabilities networkCapabilities;
+
+    /**
+     * Identifies the request.  NetworkRequests should only be constructed by
+     * the Framework and given out to applications as tokens to be used to identify
+     * the request.
+     * @hide
+     */
+    public final int requestId;
+
+    /**
+     * Set for legacy requests and the default.  Set to TYPE_NONE for none.
+     * Causes CONNECTIVITY_ACTION broadcasts to be sent.
+     * @hide
+     */
+    public final int legacyType;
+
+    /**
+     * @hide
+     */
+    public NetworkRequest(NetworkCapabilities nc, int legacyType, int rId) {
+        requestId = rId;
+        networkCapabilities = nc;
+        this.legacyType = legacyType;
+    }
+
+    /**
+     * @hide
+     */
+    public NetworkRequest(NetworkRequest that) {
+        networkCapabilities = new NetworkCapabilities(that.networkCapabilities);
+        requestId = that.requestId;
+        this.legacyType = that.legacyType;
+    }
+
+    /**
+     * Builder used to create {@link NetworkRequest} objects.  Specify the Network features
+     * needed in terms of {@link NetworkCapabilities} features
+     */
+    public static class Builder {
+        private final NetworkCapabilities mNetworkCapabilities = new NetworkCapabilities();
+
+        /**
+         * Default constructor for Builder.
+         */
+        public Builder() {}
+
+        /**
+         * Build {@link NetworkRequest} give the current set of capabilities.
+         */
+        public NetworkRequest build() {
+            return new NetworkRequest(mNetworkCapabilities, ConnectivityManager.TYPE_NONE,
+                    ConnectivityManager.REQUEST_ID_UNSET);
+        }
+
+        /**
+         * Add the given capability requirement to this builder.  These represent
+         * the requested network's required capabilities.  Note that when searching
+         * for a network to satisfy a request, all capabilities requested must be
+         * satisfied.  See {@link NetworkCapabilities} for {@code NET_CAPABILITIY_*}
+         * definitions.
+         *
+         * @param capability The {@code NetworkCapabilities.NET_CAPABILITY_*} to add.
+         * @return The builder to facilitate chaining
+         *         {@code builder.addCapability(...).addCapability();}.
+         */
+        public Builder addCapability(int capability) {
+            mNetworkCapabilities.addCapability(capability);
+            return this;
+        }
+
+        /**
+         * Removes (if found) the given capability from this builder instance.
+         *
+         * @param capability The {@code NetworkCapabilities.NET_CAPABILITY_*} to remove.
+         * @return The builder to facilitate chaining.
+         */
+        public Builder removeCapability(int capability) {
+            mNetworkCapabilities.removeCapability(capability);
+            return this;
+        }
+
+        /**
+         * Adds the given transport requirement to this builder.  These represent
+         * the set of allowed transports for the request.  Only networks using one
+         * of these transports will satisfy the request.  If no particular transports
+         * are required, none should be specified here.  See {@link NetworkCapabilities}
+         * for {@code TRANSPORT_*} definitions.
+         *
+         * @param transportType The {@code NetworkCapabilities.TRANSPORT_*} to add.
+         * @return The builder to facilitate chaining.
+         */
+        public Builder addTransportType(int transportType) {
+            mNetworkCapabilities.addTransportType(transportType);
+            return this;
+        }
+
+        /**
+         * Removes (if found) the given transport from this builder instance.
+         *
+         * @param transportType The {@code NetworkCapabilities.TRANSPORT_*} to remove.
+         * @return The builder to facilitate chaining.
+         */
+        public Builder removeTransportType(int transportType) {
+            mNetworkCapabilities.removeTransportType(transportType);
+            return this;
+        }
+
+        /**
+         * @hide
+         */
+        public Builder setLinkUpstreamBandwidthKbps(int upKbps) {
+            mNetworkCapabilities.setLinkUpstreamBandwidthKbps(upKbps);
+            return this;
+        }
+        /**
+         * @hide
+         */
+        public Builder setLinkDownstreamBandwidthKbps(int downKbps) {
+            mNetworkCapabilities.setLinkDownstreamBandwidthKbps(downKbps);
+            return this;
+        }
+    }
+
+    // implement the Parcelable interface
+    public int describeContents() {
+        return 0;
+    }
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(networkCapabilities, flags);
+        dest.writeInt(legacyType);
+        dest.writeInt(requestId);
+    }
+    public static final Creator<NetworkRequest> CREATOR =
+        new Creator<NetworkRequest>() {
+            public NetworkRequest createFromParcel(Parcel in) {
+                NetworkCapabilities nc = (NetworkCapabilities)in.readParcelable(null);
+                int legacyType = in.readInt();
+                int requestId = in.readInt();
+                NetworkRequest result = new NetworkRequest(nc, legacyType, requestId);
+                return result;
+            }
+            public NetworkRequest[] newArray(int size) {
+                return new NetworkRequest[size];
+            }
+        };
+
+    public String toString() {
+        return "NetworkRequest [ id=" + requestId + ", legacyType=" + legacyType +
+                ", " + networkCapabilities.toString() + " ]";
+    }
+
+    public boolean equals(Object obj) {
+        if (obj instanceof NetworkRequest == false) return false;
+        NetworkRequest that = (NetworkRequest)obj;
+        return (that.legacyType == this.legacyType &&
+                that.requestId == this.requestId &&
+                ((that.networkCapabilities == null && this.networkCapabilities == null) ||
+                 (that.networkCapabilities != null &&
+                  that.networkCapabilities.equals(this.networkCapabilities))));
+    }
+
+    public int hashCode() {
+        return requestId + (legacyType * 1013) +
+                (networkCapabilities.hashCode() * 1051);
+    }
+}
diff --git a/core/java/android/net/NetworkState.java b/core/java/android/net/NetworkState.java
new file mode 100644
index 0000000..2e0e9e4
--- /dev/null
+++ b/core/java/android/net/NetworkState.java
@@ -0,0 +1,84 @@
+/*
+ * 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.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Snapshot of network state.
+ *
+ * @hide
+ */
+public class NetworkState implements Parcelable {
+
+    public final NetworkInfo networkInfo;
+    public final LinkProperties linkProperties;
+    public final NetworkCapabilities networkCapabilities;
+    /** Currently only used by testing. */
+    public final String subscriberId;
+    public final String networkId;
+
+    public NetworkState(NetworkInfo networkInfo, LinkProperties linkProperties,
+            NetworkCapabilities networkCapabilities) {
+        this(networkInfo, linkProperties, networkCapabilities, null, null);
+    }
+
+    public NetworkState(NetworkInfo networkInfo, LinkProperties linkProperties,
+            NetworkCapabilities networkCapabilities, String subscriberId, String networkId) {
+        this.networkInfo = networkInfo;
+        this.linkProperties = linkProperties;
+        this.networkCapabilities = networkCapabilities;
+        this.subscriberId = subscriberId;
+        this.networkId = networkId;
+    }
+
+    public NetworkState(Parcel in) {
+        networkInfo = in.readParcelable(null);
+        linkProperties = in.readParcelable(null);
+        networkCapabilities = in.readParcelable(null);
+        subscriberId = in.readString();
+        networkId = in.readString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeParcelable(networkInfo, flags);
+        out.writeParcelable(linkProperties, flags);
+        out.writeParcelable(networkCapabilities, flags);
+        out.writeString(subscriberId);
+        out.writeString(networkId);
+    }
+
+    public static final Creator<NetworkState> CREATOR = new Creator<NetworkState>() {
+        @Override
+        public NetworkState createFromParcel(Parcel in) {
+            return new NetworkState(in);
+        }
+
+        @Override
+        public NetworkState[] newArray(int size) {
+            return new NetworkState[size];
+        }
+    };
+
+}
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
new file mode 100644
index 0000000..b02f88e
--- /dev/null
+++ b/core/java/android/net/NetworkUtils.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 java.net.InetAddress;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.Locale;
+
+import android.util.Log;
+
+/**
+ * Native methods for managing network interfaces.
+ *
+ * {@hide}
+ */
+public class NetworkUtils {
+
+    private static final String TAG = "NetworkUtils";
+
+    /** Bring the named network interface up. */
+    public native static int enableInterface(String interfaceName);
+
+    /** Bring the named network interface down. */
+    public native static int disableInterface(String interfaceName);
+
+    /** Setting bit 0 indicates reseting of IPv4 addresses required */
+    public static final int RESET_IPV4_ADDRESSES = 0x01;
+
+    /** Setting bit 1 indicates reseting of IPv4 addresses required */
+    public static final int RESET_IPV6_ADDRESSES = 0x02;
+
+    /** Reset all addresses */
+    public static final int RESET_ALL_ADDRESSES = RESET_IPV4_ADDRESSES | RESET_IPV6_ADDRESSES;
+
+    /**
+     * Reset IPv6 or IPv4 sockets that are connected via the named interface.
+     *
+     * @param interfaceName is the interface to reset
+     * @param mask {@see #RESET_IPV4_ADDRESSES} and {@see #RESET_IPV6_ADDRESSES}
+     */
+    public native static int resetConnections(String interfaceName, int mask);
+
+    /**
+     * Start the DHCP client daemon, in order to have it request addresses
+     * for the named interface, and then configure the interface with those
+     * addresses. This call blocks until it obtains a result (either success
+     * or failure) from the daemon.
+     * @param interfaceName the name of the interface to configure
+     * @param dhcpResults if the request succeeds, this object is filled in with
+     * the IP address information.
+     * @return {@code true} for success, {@code false} for failure
+     */
+    public native static boolean runDhcp(String interfaceName, DhcpResults dhcpResults);
+
+    /**
+     * Initiate renewal on the Dhcp client daemon. This call blocks until it obtains
+     * a result (either success or failure) from the daemon.
+     * @param interfaceName the name of the interface to configure
+     * @param dhcpResults if the request succeeds, this object is filled in with
+     * the IP address information.
+     * @return {@code true} for success, {@code false} for failure
+     */
+    public native static boolean runDhcpRenew(String interfaceName, DhcpResults dhcpResults);
+
+    /**
+     * Shut down the DHCP client daemon.
+     * @param interfaceName the name of the interface for which the daemon
+     * should be stopped
+     * @return {@code true} for success, {@code false} for failure
+     */
+    public native static boolean stopDhcp(String interfaceName);
+
+    /**
+     * Release the current DHCP lease.
+     * @param interfaceName the name of the interface for which the lease should
+     * be released
+     * @return {@code true} for success, {@code false} for failure
+     */
+    public native static boolean releaseDhcpLease(String interfaceName);
+
+    /**
+     * Return the last DHCP-related error message that was recorded.
+     * <p/>NOTE: This string is not localized, but currently it is only
+     * used in logging.
+     * @return the most recent error message, if any
+     */
+    public native static String getDhcpError();
+
+    /**
+     * Set the SO_MARK of {@code socketfd} to {@code mark}
+     */
+    public native static void markSocket(int socketfd, int mark);
+
+    /**
+     * Binds the current process to the network designated by {@code netId}.  All sockets created
+     * in the future (and not explicitly bound via a bound {@link SocketFactory} (see
+     * {@link Network#getSocketFactory}) will be bound to this network.  Note that if this
+     * {@code Network} ever disconnects all sockets created in this way will cease to work.  This
+     * is by design so an application doesn't accidentally use sockets it thinks are still bound to
+     * a particular {@code Network}.
+     */
+    public native static void bindProcessToNetwork(int netId);
+
+    /**
+     * Clear any process specific {@code Network} binding.  This reverts a call to
+     * {@link #bindProcessToNetwork}.
+     */
+    public native static void unbindProcessToNetwork();
+
+    /**
+     * Return the netId last passed to {@link #bindProcessToNetwork}, or NETID_UNSET if
+     * {@link #unbindProcessToNetwork} has been called since {@link #bindProcessToNetwork}.
+     */
+    public native static int getNetworkBoundToProcess();
+
+    /**
+     * Binds host resolutions performed by this process to the network designated by {@code netId}.
+     * {@link #bindProcessToNetwork} takes precedence over this setting.
+     *
+     * @deprecated This is strictly for legacy usage to support startUsingNetworkFeature().
+     */
+    public native static void bindProcessToNetworkForHostResolution(int netId);
+
+    /**
+     * Clears any process specific {@link Network} binding for host resolution.  This does
+     * not clear bindings enacted via {@link #bindProcessToNetwork}.
+     *
+     * @deprecated This is strictly for legacy usage to support startUsingNetworkFeature().
+     */
+    public native static void unbindProcessToNetworkForHostResolution();
+
+    /**
+     * Explicitly binds {@code socketfd} to the network designated by {@code netId}.  This
+     * overrides any binding via {@link #bindProcessToNetwork}.
+     */
+    public native static void bindSocketToNetwork(int socketfd, int netId);
+
+    /**
+     * Convert a IPv4 address from an integer to an InetAddress.
+     * @param hostAddress an int corresponding to the IPv4 address in network byte order
+     */
+    public static InetAddress intToInetAddress(int hostAddress) {
+        byte[] addressBytes = { (byte)(0xff & hostAddress),
+                                (byte)(0xff & (hostAddress >> 8)),
+                                (byte)(0xff & (hostAddress >> 16)),
+                                (byte)(0xff & (hostAddress >> 24)) };
+
+        try {
+           return InetAddress.getByAddress(addressBytes);
+        } catch (UnknownHostException e) {
+           throw new AssertionError();
+        }
+    }
+
+    /**
+     * Convert a IPv4 address from an InetAddress to an integer
+     * @param inetAddr is an InetAddress corresponding to the IPv4 address
+     * @return the IP address as an integer in network byte order
+     */
+    public static int inetAddressToInt(Inet4Address inetAddr)
+            throws IllegalArgumentException {
+        byte [] addr = inetAddr.getAddress();
+        return ((addr[3] & 0xff) << 24) | ((addr[2] & 0xff) << 16) |
+                ((addr[1] & 0xff) << 8) | (addr[0] & 0xff);
+    }
+
+    /**
+     * Convert a network prefix length to an IPv4 netmask integer
+     * @param prefixLength
+     * @return the IPv4 netmask as an integer in network byte order
+     */
+    public static int prefixLengthToNetmaskInt(int prefixLength)
+            throws IllegalArgumentException {
+        if (prefixLength < 0 || prefixLength > 32) {
+            throw new IllegalArgumentException("Invalid prefix length (0 <= prefix <= 32)");
+        }
+        int value = 0xffffffff << (32 - prefixLength);
+        return Integer.reverseBytes(value);
+    }
+
+    /**
+     * Convert a IPv4 netmask integer to a prefix length
+     * @param netmask as an integer in network byte order
+     * @return the network prefix length
+     */
+    public static int netmaskIntToPrefixLength(int netmask) {
+        return Integer.bitCount(netmask);
+    }
+
+    /**
+     * Create an InetAddress from a string where the string must be a standard
+     * representation of a V4 or V6 address.  Avoids doing a DNS lookup on failure
+     * but it will throw an IllegalArgumentException in that case.
+     * @param addrString
+     * @return the InetAddress
+     * @hide
+     */
+    public static InetAddress numericToInetAddress(String addrString)
+            throws IllegalArgumentException {
+        return InetAddress.parseNumericAddress(addrString);
+    }
+
+    /**
+     * Get InetAddress masked with prefixLength.  Will never return null.
+     * @param IP address which will be masked with specified prefixLength
+     * @param prefixLength the prefixLength used to mask the IP
+     */
+    public static InetAddress getNetworkPart(InetAddress address, int prefixLength) {
+        if (address == null) {
+            throw new RuntimeException("getNetworkPart doesn't accept null address");
+        }
+
+        byte[] array = address.getAddress();
+
+        if (prefixLength < 0 || prefixLength > array.length * 8) {
+            throw new RuntimeException("getNetworkPart - bad prefixLength");
+        }
+
+        int offset = prefixLength / 8;
+        int reminder = prefixLength % 8;
+        byte mask = (byte)(0xFF << (8 - reminder));
+
+        if (offset < array.length) array[offset] = (byte)(array[offset] & mask);
+
+        offset++;
+
+        for (; offset < array.length; offset++) {
+            array[offset] = 0;
+        }
+
+        InetAddress netPart = null;
+        try {
+            netPart = InetAddress.getByAddress(array);
+        } catch (UnknownHostException e) {
+            throw new RuntimeException("getNetworkPart error - " + e.toString());
+        }
+        return netPart;
+    }
+
+    /**
+     * Check if IP address type is consistent between two InetAddress.
+     * @return true if both are the same type.  False otherwise.
+     */
+    public static boolean addressTypeMatches(InetAddress left, InetAddress right) {
+        return (((left instanceof Inet4Address) && (right instanceof Inet4Address)) ||
+                ((left instanceof Inet6Address) && (right instanceof Inet6Address)));
+    }
+
+    /**
+     * Convert a 32 char hex string into a Inet6Address.
+     * throws a runtime exception if the string isn't 32 chars, isn't hex or can't be
+     * made into an Inet6Address
+     * @param addrHexString a 32 character hex string representing an IPv6 addr
+     * @return addr an InetAddress representation for the string
+     */
+    public static InetAddress hexToInet6Address(String addrHexString)
+            throws IllegalArgumentException {
+        try {
+            return numericToInetAddress(String.format(Locale.US, "%s:%s:%s:%s:%s:%s:%s:%s",
+                    addrHexString.substring(0,4),   addrHexString.substring(4,8),
+                    addrHexString.substring(8,12),  addrHexString.substring(12,16),
+                    addrHexString.substring(16,20), addrHexString.substring(20,24),
+                    addrHexString.substring(24,28), addrHexString.substring(28,32)));
+        } catch (Exception e) {
+            Log.e("NetworkUtils", "error in hexToInet6Address(" + addrHexString + "): " + e);
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Create a string array of host addresses from a collection of InetAddresses
+     * @param addrs a Collection of InetAddresses
+     * @return an array of Strings containing their host addresses
+     */
+    public static String[] makeStrings(Collection<InetAddress> addrs) {
+        String[] result = new String[addrs.size()];
+        int i = 0;
+        for (InetAddress addr : addrs) {
+            result[i++] = addr.getHostAddress();
+        }
+        return result;
+    }
+
+    /**
+     * Trim leading zeros from IPv4 address strings
+     * Our base libraries will interpret that as octel..
+     * Must leave non v4 addresses and host names alone.
+     * For example, 192.168.000.010 -> 192.168.0.10
+     * TODO - fix base libraries and remove this function
+     * @param addr a string representing an ip addr
+     * @return a string propertly trimmed
+     */
+    public static String trimV4AddrZeros(String addr) {
+        if (addr == null) return null;
+        String[] octets = addr.split("\\.");
+        if (octets.length != 4) return addr;
+        StringBuilder builder = new StringBuilder(16);
+        String result = null;
+        for (int i = 0; i < 4; i++) {
+            try {
+                if (octets[i].length() > 3) return addr;
+                builder.append(Integer.parseInt(octets[i]));
+            } catch (NumberFormatException e) {
+                return addr;
+            }
+            if (i < 3) builder.append('.');
+        }
+        result = builder.toString();
+        return result;
+    }
+}
diff --git a/core/java/android/net/ProxyInfo.java b/core/java/android/net/ProxyInfo.java
new file mode 100644
index 0000000..7ea6bae
--- /dev/null
+++ b/core/java/android/net/ProxyInfo.java
@@ -0,0 +1,366 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import org.apache.http.client.HttpClient;
+
+import java.net.InetSocketAddress;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Describes a proxy configuration.
+ *
+ * Proxy configurations are already integrated within the Apache HTTP stack.
+ * So {@link URLConnection} and {@link HttpClient} will use them automatically.
+ *
+ * Other HTTP stacks will need to obtain the proxy info from
+ * {@link Proxy#PROXY_CHANGE_ACTION} broadcast as the extra {@link Proxy#EXTRA_PROXY_INFO}.
+ */
+public class ProxyInfo implements Parcelable {
+
+    private String mHost;
+    private int mPort;
+    private String mExclusionList;
+    private String[] mParsedExclusionList;
+
+    private Uri mPacFileUrl;
+    /**
+     *@hide
+     */
+    public static final String LOCAL_EXCL_LIST = "";
+    /**
+     *@hide
+     */
+    public static final int LOCAL_PORT = -1;
+    /**
+     *@hide
+     */
+    public static final String LOCAL_HOST = "localhost";
+
+    /**
+     * Constructs a {@link ProxyInfo} object that points at a Direct proxy
+     * on the specified host and port.
+     */
+    public static ProxyInfo buildDirectProxy(String host, int port) {
+        return new ProxyInfo(host, port, null);
+    }
+
+    /**
+     * Constructs a {@link ProxyInfo} object that points at a Direct proxy
+     * on the specified host and port.
+     *
+     * The proxy will not be used to access any host in exclusion list, exclList.
+     *
+     * @param exclList Hosts to exclude using the proxy on connections for.  These
+     *                 hosts can use wildcards such as *.example.com.
+     */
+    public static ProxyInfo buildDirectProxy(String host, int port, List<String> exclList) {
+        String[] array = exclList.toArray(new String[exclList.size()]);
+        return new ProxyInfo(host, port, TextUtils.join(",", array), array);
+    }
+
+    /**
+     * Construct a {@link ProxyInfo} that will download and run the PAC script
+     * at the specified URL.
+     */
+    public static ProxyInfo buildPacProxy(Uri pacUri) {
+        return new ProxyInfo(pacUri);
+    }
+
+    /**
+     * Create a ProxyProperties that points at a HTTP Proxy.
+     * @hide
+     */
+    public ProxyInfo(String host, int port, String exclList) {
+        mHost = host;
+        mPort = port;
+        setExclusionList(exclList);
+        mPacFileUrl = Uri.EMPTY;
+    }
+
+    /**
+     * Create a ProxyProperties that points at a PAC URL.
+     * @hide
+     */
+    public ProxyInfo(Uri pacFileUrl) {
+        mHost = LOCAL_HOST;
+        mPort = LOCAL_PORT;
+        setExclusionList(LOCAL_EXCL_LIST);
+        if (pacFileUrl == null) {
+            throw new NullPointerException();
+        }
+        mPacFileUrl = pacFileUrl;
+    }
+
+    /**
+     * Create a ProxyProperties that points at a PAC URL.
+     * @hide
+     */
+    public ProxyInfo(String pacFileUrl) {
+        mHost = LOCAL_HOST;
+        mPort = LOCAL_PORT;
+        setExclusionList(LOCAL_EXCL_LIST);
+        mPacFileUrl = Uri.parse(pacFileUrl);
+    }
+
+    /**
+     * Only used in PacManager after Local Proxy is bound.
+     * @hide
+     */
+    public ProxyInfo(Uri pacFileUrl, int localProxyPort) {
+        mHost = LOCAL_HOST;
+        mPort = localProxyPort;
+        setExclusionList(LOCAL_EXCL_LIST);
+        if (pacFileUrl == null) {
+            throw new NullPointerException();
+        }
+        mPacFileUrl = pacFileUrl;
+    }
+
+    private ProxyInfo(String host, int port, String exclList, String[] parsedExclList) {
+        mHost = host;
+        mPort = port;
+        mExclusionList = exclList;
+        mParsedExclusionList = parsedExclList;
+        mPacFileUrl = Uri.EMPTY;
+    }
+
+    // copy constructor instead of clone
+    /**
+     * @hide
+     */
+    public ProxyInfo(ProxyInfo source) {
+        if (source != null) {
+            mHost = source.getHost();
+            mPort = source.getPort();
+            mPacFileUrl = source.mPacFileUrl;
+            mExclusionList = source.getExclusionListAsString();
+            mParsedExclusionList = source.mParsedExclusionList;
+        } else {
+            mPacFileUrl = Uri.EMPTY;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public InetSocketAddress getSocketAddress() {
+        InetSocketAddress inetSocketAddress = null;
+        try {
+            inetSocketAddress = new InetSocketAddress(mHost, mPort);
+        } catch (IllegalArgumentException e) { }
+        return inetSocketAddress;
+    }
+
+    /**
+     * Returns the URL of the current PAC script or null if there is
+     * no PAC script.
+     */
+    public Uri getPacFileUrl() {
+        return mPacFileUrl;
+    }
+
+    /**
+     * When configured to use a Direct Proxy this returns the host
+     * of the proxy.
+     */
+    public String getHost() {
+        return mHost;
+    }
+
+    /**
+     * When configured to use a Direct Proxy this returns the port
+     * of the proxy
+     */
+    public int getPort() {
+        return mPort;
+    }
+
+    /**
+     * When configured to use a Direct Proxy this returns the list
+     * of hosts for which the proxy is ignored.
+     */
+    public String[] getExclusionList() {
+        return mParsedExclusionList;
+    }
+
+    /**
+     * comma separated
+     * @hide
+     */
+    public String getExclusionListAsString() {
+        return mExclusionList;
+    }
+
+    // comma separated
+    private void setExclusionList(String exclusionList) {
+        mExclusionList = exclusionList;
+        if (mExclusionList == null) {
+            mParsedExclusionList = new String[0];
+        } else {
+            mParsedExclusionList = exclusionList.toLowerCase(Locale.ROOT).split(",");
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isValid() {
+        if (!Uri.EMPTY.equals(mPacFileUrl)) return true;
+        return Proxy.PROXY_VALID == Proxy.validate(mHost == null ? "" : mHost,
+                                                mPort == 0 ? "" : Integer.toString(mPort),
+                                                mExclusionList == null ? "" : mExclusionList);
+    }
+
+    /**
+     * @hide
+     */
+    public java.net.Proxy makeProxy() {
+        java.net.Proxy proxy = java.net.Proxy.NO_PROXY;
+        if (mHost != null) {
+            try {
+                InetSocketAddress inetSocketAddress = new InetSocketAddress(mHost, mPort);
+                proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, inetSocketAddress);
+            } catch (IllegalArgumentException e) {
+            }
+        }
+        return proxy;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        if (!Uri.EMPTY.equals(mPacFileUrl)) {
+            sb.append("PAC Script: ");
+            sb.append(mPacFileUrl);
+        } else if (mHost != null) {
+            sb.append("[");
+            sb.append(mHost);
+            sb.append("] ");
+            sb.append(Integer.toString(mPort));
+            if (mExclusionList != null) {
+                    sb.append(" xl=").append(mExclusionList);
+            }
+        } else {
+            sb.append("[ProxyProperties.mHost == null]");
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof ProxyInfo)) return false;
+        ProxyInfo p = (ProxyInfo)o;
+        // If PAC URL is present in either then they must be equal.
+        // Other parameters will only be for fall back.
+        if (!Uri.EMPTY.equals(mPacFileUrl)) {
+            return mPacFileUrl.equals(p.getPacFileUrl()) && mPort == p.mPort;
+        }
+        if (!Uri.EMPTY.equals(p.mPacFileUrl)) {
+            return false;
+        }
+        if (mExclusionList != null && !mExclusionList.equals(p.getExclusionListAsString())) {
+            return false;
+        }
+        if (mHost != null && p.getHost() != null && mHost.equals(p.getHost()) == false) {
+            return false;
+        }
+        if (mHost != null && p.mHost == null) return false;
+        if (mHost == null && p.mHost != null) return false;
+        if (mPort != p.mPort) return false;
+        return true;
+    }
+
+    /**
+     * Implement the Parcelable interface
+     * @hide
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    /*
+     * generate hashcode based on significant fields
+     */
+    public int hashCode() {
+        return ((null == mHost) ? 0 : mHost.hashCode())
+        + ((null == mExclusionList) ? 0 : mExclusionList.hashCode())
+        + mPort;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        if (!Uri.EMPTY.equals(mPacFileUrl)) {
+            dest.writeByte((byte)1);
+            mPacFileUrl.writeToParcel(dest, 0);
+            dest.writeInt(mPort);
+            return;
+        } else {
+            dest.writeByte((byte)0);
+        }
+        if (mHost != null) {
+            dest.writeByte((byte)1);
+            dest.writeString(mHost);
+            dest.writeInt(mPort);
+        } else {
+            dest.writeByte((byte)0);
+        }
+        dest.writeString(mExclusionList);
+        dest.writeStringArray(mParsedExclusionList);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public static final Creator<ProxyInfo> CREATOR =
+        new Creator<ProxyInfo>() {
+            public ProxyInfo createFromParcel(Parcel in) {
+                String host = null;
+                int port = 0;
+                if (in.readByte() != 0) {
+                    Uri url = Uri.CREATOR.createFromParcel(in);
+                    int localPort = in.readInt();
+                    return new ProxyInfo(url, localPort);
+                }
+                if (in.readByte() != 0) {
+                    host = in.readString();
+                    port = in.readInt();
+                }
+                String exclList = in.readString();
+                String[] parsedExclList = in.readStringArray();
+                ProxyInfo proxyProperties =
+                        new ProxyInfo(host, port, exclList, parsedExclList);
+                return proxyProperties;
+            }
+
+            public ProxyInfo[] newArray(int size) {
+                return new ProxyInfo[size];
+            }
+        };
+}
diff --git a/core/java/android/net/RouteInfo.java b/core/java/android/net/RouteInfo.java
new file mode 100644
index 0000000..8b42bcd
--- /dev/null
+++ b/core/java/android/net/RouteInfo.java
@@ -0,0 +1,453 @@
+/*
+ * 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.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.UnknownHostException;
+import java.net.InetAddress;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * Represents a network route.
+ * <p>
+ * This is used both to describe static network configuration and live network
+ * configuration information.
+ *
+ * A route contains three pieces of information:
+ * <ul>
+ * <li>a destination {@link IpPrefix} specifying the network destinations covered by this route.
+ *     If this is {@code null} it indicates a default route of the address family (IPv4 or IPv6)
+ *     implied by the gateway IP address.
+ * <li>a gateway {@link InetAddress} indicating the next hop to use.  If this is {@code null} it
+ *     indicates a directly-connected route.
+ * <li>an interface (which may be unspecified).
+ * </ul>
+ * Either the destination or the gateway may be {@code null}, but not both.  If the
+ * destination and gateway are both specified, they must be of the same address family
+ * (IPv4 or IPv6).
+ */
+public class RouteInfo implements Parcelable {
+    /**
+     * The IP destination address for this route.
+     * TODO: Make this an IpPrefix.
+     */
+    private final LinkAddress mDestination;
+
+    /**
+     * The gateway address for this route.
+     */
+    private final InetAddress mGateway;
+
+    /**
+     * The interface for this route.
+     */
+    private final String mInterface;
+
+    private final boolean mIsDefault;
+    private final boolean mIsHost;
+    private final boolean mHasGateway;
+
+    /**
+     * Constructs a RouteInfo object.
+     *
+     * If destination is null, then gateway must be specified and the
+     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
+     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
+     * route <code>::/0</code> if gateway is an instance of
+     * {@link Inet6Address}.
+     * <p>
+     * destination and gateway may not both be null.
+     *
+     * @param destination the destination prefix
+     * @param gateway the IP address to route packets through
+     * @param iface the interface name to send packets on
+     *
+     * TODO: Convert to use IpPrefix.
+     *
+     * @hide
+     */
+    public RouteInfo(IpPrefix destination, InetAddress gateway, String iface) {
+        this(destination == null ? null :
+                new LinkAddress(destination.getAddress(), destination.getPrefixLength()),
+                gateway, iface);
+    }
+
+    /**
+     * @hide
+     */
+    public RouteInfo(LinkAddress destination, InetAddress gateway, String iface) {
+        if (destination == null) {
+            if (gateway != null) {
+                if (gateway instanceof Inet4Address) {
+                    destination = new LinkAddress(Inet4Address.ANY, 0);
+                } else {
+                    destination = new LinkAddress(Inet6Address.ANY, 0);
+                }
+            } else {
+                // no destination, no gateway. invalid.
+                throw new IllegalArgumentException("Invalid arguments passed in: " + gateway + "," +
+                                                   destination);
+            }
+        }
+        if (gateway == null) {
+            if (destination.getAddress() instanceof Inet4Address) {
+                gateway = Inet4Address.ANY;
+            } else {
+                gateway = Inet6Address.ANY;
+            }
+        }
+        mHasGateway = (!gateway.isAnyLocalAddress());
+
+        mDestination = new LinkAddress(NetworkUtils.getNetworkPart(destination.getAddress(),
+                destination.getPrefixLength()), destination.getPrefixLength());
+        if ((destination.getAddress() instanceof Inet4Address &&
+                 (gateway instanceof Inet4Address == false)) ||
+                (destination.getAddress() instanceof Inet6Address &&
+                 (gateway instanceof Inet6Address == false))) {
+            throw new IllegalArgumentException("address family mismatch in RouteInfo constructor");
+        }
+        mGateway = gateway;
+        mInterface = iface;
+        mIsDefault = isDefault();
+        mIsHost = isHost();
+    }
+
+    /**
+     * Constructs a {@code RouteInfo} object.
+     *
+     * If destination is null, then gateway must be specified and the
+     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
+     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
+     * route <code>::/0</code> if gateway is an instance of {@link Inet6Address}.
+     * <p>
+     * Destination and gateway may not both be null.
+     *
+     * @param destination the destination address and prefix in an {@link IpPrefix}
+     * @param gateway the {@link InetAddress} to route packets through
+     *
+     * @hide
+     */
+    public RouteInfo(IpPrefix destination, InetAddress gateway) {
+        this(destination, gateway, null);
+    }
+
+    /**
+     * @hide
+     */
+    public RouteInfo(LinkAddress destination, InetAddress gateway) {
+        this(destination, gateway, null);
+    }
+
+    /**
+     * Constructs a default {@code RouteInfo} object.
+     *
+     * @param gateway the {@link InetAddress} to route packets through
+     *
+     * @hide
+     */
+    public RouteInfo(InetAddress gateway) {
+        this((LinkAddress) null, gateway, null);
+    }
+
+    /**
+     * Constructs a {@code RouteInfo} object representing a direct connected subnet.
+     *
+     * @param destination the {@link IpPrefix} describing the address and prefix
+     *                    length of the subnet.
+     *
+     * @hide
+     */
+    public RouteInfo(IpPrefix destination) {
+        this(destination, null, null);
+    }
+
+    /**
+     * @hide
+     */
+    public RouteInfo(LinkAddress destination) {
+        this(destination, null, null);
+    }
+
+    /**
+     * @hide
+     */
+    public static RouteInfo makeHostRoute(InetAddress host, String iface) {
+        return makeHostRoute(host, null, iface);
+    }
+
+    /**
+     * @hide
+     */
+    public static RouteInfo makeHostRoute(InetAddress host, InetAddress gateway, String iface) {
+        if (host == null) return null;
+
+        if (host instanceof Inet4Address) {
+            return new RouteInfo(new LinkAddress(host, 32), gateway, iface);
+        } else {
+            return new RouteInfo(new LinkAddress(host, 128), gateway, iface);
+        }
+    }
+
+    private boolean isHost() {
+        return (mDestination.getAddress() instanceof Inet4Address &&
+                mDestination.getPrefixLength() == 32) ||
+               (mDestination.getAddress() instanceof Inet6Address &&
+                mDestination.getPrefixLength() == 128);
+    }
+
+    private boolean isDefault() {
+        boolean val = false;
+        if (mGateway != null) {
+            if (mGateway instanceof Inet4Address) {
+                val = (mDestination == null || mDestination.getPrefixLength() == 0);
+            } else {
+                val = (mDestination == null || mDestination.getPrefixLength() == 0);
+            }
+        }
+        return val;
+    }
+
+    /**
+     * Retrieves the destination address and prefix length in the form of an {@link IpPrefix}.
+     *
+     * @return {@link IpPrefix} specifying the destination.  This is never {@code null}.
+     */
+    public IpPrefix getDestination() {
+        return new IpPrefix(mDestination.getAddress(), mDestination.getPrefixLength());
+    }
+
+    /**
+     * TODO: Convert callers to use IpPrefix and then remove.
+     * @hide
+     */
+    public LinkAddress getDestinationLinkAddress() {
+        return mDestination;
+    }
+
+    /**
+     * Retrieves the gateway or next hop {@link InetAddress} for this route.
+     *
+     * @return {@link InetAddress} specifying the gateway or next hop.  This may be
+     &                             {@code null} for a directly-connected route."
+     */
+    public InetAddress getGateway() {
+        return mGateway;
+    }
+
+    /**
+     * Retrieves the interface used for this route if specified, else {@code null}.
+     *
+     * @return The name of the interface used for this route.
+     */
+    public String getInterface() {
+        return mInterface;
+    }
+
+    /**
+     * Indicates if this route is a default route (ie, has no destination specified).
+     *
+     * @return {@code true} if the destination has a prefix length of 0.
+     */
+    public boolean isDefaultRoute() {
+        return mIsDefault;
+    }
+
+    /**
+     * Indicates if this route is a host route (ie, matches only a single host address).
+     *
+     * @return {@code true} if the destination has a prefix length of 32 or 128 for IPv4 or IPv6,
+     * respectively.
+     * @hide
+     */
+    public boolean isHostRoute() {
+        return mIsHost;
+    }
+
+    /**
+     * Indicates if this route has a next hop ({@code true}) or is directly-connected
+     * ({@code false}).
+     *
+     * @return {@code true} if a gateway is specified
+     * @hide
+     */
+    public boolean hasGateway() {
+        return mHasGateway;
+    }
+
+    /**
+     * Determines whether the destination and prefix of this route includes the specified
+     * address.
+     *
+     * @param destination A {@link InetAddress} to test to see if it would match this route.
+     * @return {@code true} if the destination and prefix length cover the given address.
+     */
+    public boolean matches(InetAddress destination) {
+        if (destination == null) return false;
+
+        // match the route destination and destination with prefix length
+        InetAddress dstNet = NetworkUtils.getNetworkPart(destination,
+                mDestination.getPrefixLength());
+
+        return mDestination.getAddress().equals(dstNet);
+    }
+
+    /**
+     * Find the route from a Collection of routes that best matches a given address.
+     * May return null if no routes are applicable.
+     * @param routes a Collection of RouteInfos to chose from
+     * @param dest the InetAddress your trying to get to
+     * @return the RouteInfo from the Collection that best fits the given address
+     *
+     * @hide
+     */
+    public static RouteInfo selectBestRoute(Collection<RouteInfo> routes, InetAddress dest) {
+        if ((routes == null) || (dest == null)) return null;
+
+        RouteInfo bestRoute = null;
+        // pick a longest prefix match under same address type
+        for (RouteInfo route : routes) {
+            if (NetworkUtils.addressTypeMatches(route.mDestination.getAddress(), dest)) {
+                if ((bestRoute != null) &&
+                        (bestRoute.mDestination.getPrefixLength() >=
+                        route.mDestination.getPrefixLength())) {
+                    continue;
+                }
+                if (route.matches(dest)) bestRoute = route;
+            }
+        }
+        return bestRoute;
+    }
+
+    /**
+     * Returns a human-readable description of this object.
+     */
+    public String toString() {
+        String val = "";
+        if (mDestination != null) val = mDestination.toString();
+        val += " ->";
+        if (mGateway != null) val += " " + mGateway.getHostAddress();
+        if (mInterface != null) val += " " + mInterface;
+        return val;
+    }
+
+    /**
+     * Compares this RouteInfo object against the specified object and indicates if they are equal.
+     * @return {@code true} if the objects are equal, {@code false} otherwise.
+     */
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof RouteInfo)) return false;
+
+        RouteInfo target = (RouteInfo) obj;
+
+        return Objects.equals(mDestination, target.getDestinationLinkAddress()) &&
+                Objects.equals(mGateway, target.getGateway()) &&
+                Objects.equals(mInterface, target.getInterface());
+    }
+
+    /**
+     *  Returns a hashcode for this <code>RouteInfo</code> object.
+     */
+    public int hashCode() {
+        return (mDestination == null ? 0 : mDestination.hashCode() * 41)
+                + (mGateway == null ? 0 :mGateway.hashCode() * 47)
+                + (mInterface == null ? 0 :mInterface.hashCode() * 67)
+                + (mIsDefault ? 3 : 7);
+    }
+
+    /**
+     * Implement the Parcelable interface
+     * @hide
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implement the Parcelable interface
+     * @hide
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        if (mDestination == null) {
+            dest.writeByte((byte) 0);
+        } else {
+            dest.writeByte((byte) 1);
+            dest.writeByteArray(mDestination.getAddress().getAddress());
+            dest.writeInt(mDestination.getPrefixLength());
+        }
+
+        if (mGateway == null) {
+            dest.writeByte((byte) 0);
+        } else {
+            dest.writeByte((byte) 1);
+            dest.writeByteArray(mGateway.getAddress());
+        }
+
+        dest.writeString(mInterface);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public static final Creator<RouteInfo> CREATOR =
+        new Creator<RouteInfo>() {
+        public RouteInfo createFromParcel(Parcel in) {
+            InetAddress destAddr = null;
+            int prefix = 0;
+            InetAddress gateway = null;
+
+            if (in.readByte() == 1) {
+                byte[] addr = in.createByteArray();
+                prefix = in.readInt();
+
+                try {
+                    destAddr = InetAddress.getByAddress(addr);
+                } catch (UnknownHostException e) {}
+            }
+
+            if (in.readByte() == 1) {
+                byte[] addr = in.createByteArray();
+
+                try {
+                    gateway = InetAddress.getByAddress(addr);
+                } catch (UnknownHostException e) {}
+            }
+
+            String iface = in.readString();
+
+            LinkAddress dest = null;
+
+            if (destAddr != null) {
+                dest = new LinkAddress(destAddr, prefix);
+            }
+
+            return new RouteInfo(dest, gateway, iface);
+        }
+
+        public RouteInfo[] newArray(int size) {
+            return new RouteInfo[size];
+        }
+    };
+}
diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp
new file mode 100644
index 0000000..bc5e1b3
--- /dev/null
+++ b/core/jni/android_net_NetUtils.cpp
@@ -0,0 +1,337 @@
+/*
+ * Copyright 2008, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 "NetUtils"
+
+#include "jni.h"
+#include "JNIHelp.h"
+#include "NetdClient.h"
+#include "resolv_netid.h"
+#include <utils/misc.h>
+#include <android_runtime/AndroidRuntime.h>
+#include <utils/Log.h>
+#include <arpa/inet.h>
+#include <cutils/properties.h>
+
+extern "C" {
+int ifc_enable(const char *ifname);
+int ifc_disable(const char *ifname);
+int ifc_reset_connections(const char *ifname, int reset_mask);
+
+int dhcp_do_request(const char * const ifname,
+                    const char *ipaddr,
+                    const char *gateway,
+                    uint32_t *prefixLength,
+                    const char *dns[],
+                    const char *server,
+                    uint32_t *lease,
+                    const char *vendorInfo,
+                    const char *domains,
+                    const char *mtu);
+
+int dhcp_do_request_renew(const char * const ifname,
+                    const char *ipaddr,
+                    const char *gateway,
+                    uint32_t *prefixLength,
+                    const char *dns[],
+                    const char *server,
+                    uint32_t *lease,
+                    const char *vendorInfo,
+                    const char *domains,
+                    const char *mtu);
+
+int dhcp_stop(const char *ifname);
+int dhcp_release_lease(const char *ifname);
+char *dhcp_get_errmsg();
+}
+
+#define NETUTILS_PKG_NAME "android/net/NetworkUtils"
+
+namespace android {
+
+/*
+ * The following remembers the jfieldID's of the fields
+ * of the DhcpInfo Java object, so that we don't have
+ * to look them up every time.
+ */
+static struct fieldIds {
+    jmethodID clear;
+    jmethodID setInterfaceName;
+    jmethodID addLinkAddress;
+    jmethodID addGateway;
+    jmethodID addDns;
+    jmethodID setDomains;
+    jmethodID setServerAddress;
+    jmethodID setLeaseDuration;
+    jmethodID setVendorInfo;
+} dhcpResultsFieldIds;
+
+static jint android_net_utils_enableInterface(JNIEnv* env, jobject clazz, jstring ifname)
+{
+    int result;
+
+    const char *nameStr = env->GetStringUTFChars(ifname, NULL);
+    result = ::ifc_enable(nameStr);
+    env->ReleaseStringUTFChars(ifname, nameStr);
+    return (jint)result;
+}
+
+static jint android_net_utils_disableInterface(JNIEnv* env, jobject clazz, jstring ifname)
+{
+    int result;
+
+    const char *nameStr = env->GetStringUTFChars(ifname, NULL);
+    result = ::ifc_disable(nameStr);
+    env->ReleaseStringUTFChars(ifname, nameStr);
+    return (jint)result;
+}
+
+static jint android_net_utils_resetConnections(JNIEnv* env, jobject clazz,
+      jstring ifname, jint mask)
+{
+    int result;
+
+    const char *nameStr = env->GetStringUTFChars(ifname, NULL);
+
+    ALOGD("android_net_utils_resetConnections in env=%p clazz=%p iface=%s mask=0x%x\n",
+          env, clazz, nameStr, mask);
+
+    result = ::ifc_reset_connections(nameStr, mask);
+    env->ReleaseStringUTFChars(ifname, nameStr);
+    return (jint)result;
+}
+
+static jboolean android_net_utils_runDhcpCommon(JNIEnv* env, jobject clazz, jstring ifname,
+        jobject dhcpResults, bool renew)
+{
+    int result;
+    char  ipaddr[PROPERTY_VALUE_MAX];
+    uint32_t prefixLength;
+    char gateway[PROPERTY_VALUE_MAX];
+    char    dns1[PROPERTY_VALUE_MAX];
+    char    dns2[PROPERTY_VALUE_MAX];
+    char    dns3[PROPERTY_VALUE_MAX];
+    char    dns4[PROPERTY_VALUE_MAX];
+    const char *dns[5] = {dns1, dns2, dns3, dns4, NULL};
+    char  server[PROPERTY_VALUE_MAX];
+    uint32_t lease;
+    char vendorInfo[PROPERTY_VALUE_MAX];
+    char domains[PROPERTY_VALUE_MAX];
+    char mtu[PROPERTY_VALUE_MAX];
+
+    const char *nameStr = env->GetStringUTFChars(ifname, NULL);
+    if (nameStr == NULL) return (jboolean)false;
+
+    if (renew) {
+        result = ::dhcp_do_request_renew(nameStr, ipaddr, gateway, &prefixLength,
+                dns, server, &lease, vendorInfo, domains, mtu);
+    } else {
+        result = ::dhcp_do_request(nameStr, ipaddr, gateway, &prefixLength,
+                dns, server, &lease, vendorInfo, domains, mtu);
+    }
+    if (result != 0) {
+        ALOGD("dhcp_do_request failed : %s (%s)", nameStr, renew ? "renew" : "new");
+    }
+
+    env->ReleaseStringUTFChars(ifname, nameStr);
+    if (result == 0) {
+        env->CallVoidMethod(dhcpResults, dhcpResultsFieldIds.clear);
+
+        // set mIfaceName
+        // dhcpResults->setInterfaceName(ifname)
+        env->CallVoidMethod(dhcpResults, dhcpResultsFieldIds.setInterfaceName, ifname);
+
+        // set the linkAddress
+        // dhcpResults->addLinkAddress(inetAddress, prefixLength)
+        result = env->CallBooleanMethod(dhcpResults, dhcpResultsFieldIds.addLinkAddress,
+                env->NewStringUTF(ipaddr), prefixLength);
+    }
+
+    if (result == 0) {
+        // set the gateway
+        // dhcpResults->addGateway(gateway)
+        result = env->CallBooleanMethod(dhcpResults,
+                dhcpResultsFieldIds.addGateway, env->NewStringUTF(gateway));
+    }
+
+    if (result == 0) {
+        // dhcpResults->addDns(new InetAddress(dns1))
+        result = env->CallBooleanMethod(dhcpResults,
+                dhcpResultsFieldIds.addDns, env->NewStringUTF(dns1));
+    }
+
+    if (result == 0) {
+        env->CallVoidMethod(dhcpResults, dhcpResultsFieldIds.setDomains,
+                env->NewStringUTF(domains));
+
+        result = env->CallBooleanMethod(dhcpResults,
+                dhcpResultsFieldIds.addDns, env->NewStringUTF(dns2));
+
+        if (result == 0) {
+            result = env->CallBooleanMethod(dhcpResults,
+                    dhcpResultsFieldIds.addDns, env->NewStringUTF(dns3));
+            if (result == 0) {
+                result = env->CallBooleanMethod(dhcpResults,
+                        dhcpResultsFieldIds.addDns, env->NewStringUTF(dns4));
+            }
+        }
+    }
+
+    if (result == 0) {
+        // dhcpResults->setServerAddress(new InetAddress(server))
+        result = env->CallBooleanMethod(dhcpResults, dhcpResultsFieldIds.setServerAddress,
+                env->NewStringUTF(server));
+    }
+
+    if (result == 0) {
+        // dhcpResults->setLeaseDuration(lease)
+        env->CallVoidMethod(dhcpResults,
+                dhcpResultsFieldIds.setLeaseDuration, lease);
+
+        // dhcpResults->setVendorInfo(vendorInfo)
+        env->CallVoidMethod(dhcpResults, dhcpResultsFieldIds.setVendorInfo,
+                env->NewStringUTF(vendorInfo));
+    }
+    return (jboolean)(result == 0);
+}
+
+
+static jboolean android_net_utils_runDhcp(JNIEnv* env, jobject clazz, jstring ifname, jobject info)
+{
+    return android_net_utils_runDhcpCommon(env, clazz, ifname, info, false);
+}
+
+static jboolean android_net_utils_runDhcpRenew(JNIEnv* env, jobject clazz, jstring ifname, jobject info)
+{
+    return android_net_utils_runDhcpCommon(env, clazz, ifname, info, true);
+}
+
+
+static jboolean android_net_utils_stopDhcp(JNIEnv* env, jobject clazz, jstring ifname)
+{
+    int result;
+
+    const char *nameStr = env->GetStringUTFChars(ifname, NULL);
+    result = ::dhcp_stop(nameStr);
+    env->ReleaseStringUTFChars(ifname, nameStr);
+    return (jboolean)(result == 0);
+}
+
+static jboolean android_net_utils_releaseDhcpLease(JNIEnv* env, jobject clazz, jstring ifname)
+{
+    int result;
+
+    const char *nameStr = env->GetStringUTFChars(ifname, NULL);
+    result = ::dhcp_release_lease(nameStr);
+    env->ReleaseStringUTFChars(ifname, nameStr);
+    return (jboolean)(result == 0);
+}
+
+static jstring android_net_utils_getDhcpError(JNIEnv* env, jobject clazz)
+{
+    return env->NewStringUTF(::dhcp_get_errmsg());
+}
+
+static void android_net_utils_markSocket(JNIEnv *env, jobject thiz, jint socket, jint mark)
+{
+    if (setsockopt(socket, SOL_SOCKET, SO_MARK, &mark, sizeof(mark)) < 0) {
+        jniThrowException(env, "java/lang/IllegalStateException", "Error marking socket");
+    }
+}
+
+static void android_net_utils_bindProcessToNetwork(JNIEnv *env, jobject thiz, jint netId)
+{
+    setNetworkForProcess(netId);
+}
+
+static void android_net_utils_unbindProcessToNetwork(JNIEnv *env, jobject thiz)
+{
+    setNetworkForProcess(NETID_UNSET);
+}
+
+static jint android_net_utils_getNetworkBoundToProcess(JNIEnv *env, jobject thiz)
+{
+    return getNetworkForProcess();
+}
+
+static void android_net_utils_bindProcessToNetworkForHostResolution(JNIEnv *env, jobject thiz, jint netId)
+{
+    setNetworkForResolv(netId);
+}
+
+static void android_net_utils_unbindProcessToNetworkForHostResolution(JNIEnv *env, jobject thiz)
+{
+    setNetworkForResolv(NETID_UNSET);
+}
+
+static void android_net_utils_bindSocketToNetwork(JNIEnv *env, jobject thiz, jint socket, jint netId)
+{
+    setNetworkForSocket(netId, socket);
+}
+
+// ----------------------------------------------------------------------------
+
+/*
+ * JNI registration.
+ */
+static JNINativeMethod gNetworkUtilMethods[] = {
+    /* name, signature, funcPtr */
+
+    { "enableInterface", "(Ljava/lang/String;)I",  (void *)android_net_utils_enableInterface },
+    { "disableInterface", "(Ljava/lang/String;)I",  (void *)android_net_utils_disableInterface },
+    { "resetConnections", "(Ljava/lang/String;I)I",  (void *)android_net_utils_resetConnections },
+    { "runDhcp", "(Ljava/lang/String;Landroid/net/DhcpResults;)Z",  (void *)android_net_utils_runDhcp },
+    { "runDhcpRenew", "(Ljava/lang/String;Landroid/net/DhcpResults;)Z",  (void *)android_net_utils_runDhcpRenew },
+    { "stopDhcp", "(Ljava/lang/String;)Z",  (void *)android_net_utils_stopDhcp },
+    { "releaseDhcpLease", "(Ljava/lang/String;)Z",  (void *)android_net_utils_releaseDhcpLease },
+    { "getDhcpError", "()Ljava/lang/String;", (void*) android_net_utils_getDhcpError },
+    { "markSocket", "(II)V", (void*) android_net_utils_markSocket },
+    { "bindProcessToNetwork", "(I)V", (void*) android_net_utils_bindProcessToNetwork },
+    { "getNetworkBoundToProcess", "()I", (void*) android_net_utils_getNetworkBoundToProcess },
+    { "unbindProcessToNetwork", "()V", (void*) android_net_utils_unbindProcessToNetwork },
+    { "bindProcessToNetworkForHostResolution", "(I)V", (void*) android_net_utils_bindProcessToNetworkForHostResolution },
+    { "unbindProcessToNetworkForHostResolution", "()V", (void*) android_net_utils_unbindProcessToNetworkForHostResolution },
+    { "bindSocketToNetwork", "(II)V", (void*) android_net_utils_bindSocketToNetwork },
+};
+
+int register_android_net_NetworkUtils(JNIEnv* env)
+{
+    jclass dhcpResultsClass = env->FindClass("android/net/DhcpResults");
+    LOG_FATAL_IF(dhcpResultsClass == NULL, "Unable to find class android/net/DhcpResults");
+    dhcpResultsFieldIds.clear =
+            env->GetMethodID(dhcpResultsClass, "clear", "()V");
+    dhcpResultsFieldIds.setInterfaceName =
+            env->GetMethodID(dhcpResultsClass, "setInterfaceName", "(Ljava/lang/String;)V");
+    dhcpResultsFieldIds.addLinkAddress =
+            env->GetMethodID(dhcpResultsClass, "addLinkAddress", "(Ljava/lang/String;I)Z");
+    dhcpResultsFieldIds.addGateway =
+            env->GetMethodID(dhcpResultsClass, "addGateway", "(Ljava/lang/String;)Z");
+    dhcpResultsFieldIds.addDns =
+            env->GetMethodID(dhcpResultsClass, "addDns", "(Ljava/lang/String;)Z");
+    dhcpResultsFieldIds.setDomains =
+            env->GetMethodID(dhcpResultsClass, "setDomains", "(Ljava/lang/String;)V");
+    dhcpResultsFieldIds.setServerAddress =
+            env->GetMethodID(dhcpResultsClass, "setServerAddress", "(Ljava/lang/String;)Z");
+    dhcpResultsFieldIds.setLeaseDuration =
+            env->GetMethodID(dhcpResultsClass, "setLeaseDuration", "(I)V");
+    dhcpResultsFieldIds.setVendorInfo =
+            env->GetMethodID(dhcpResultsClass, "setVendorInfo", "(Ljava/lang/String;)V");
+
+    return AndroidRuntime::registerNativeMethods(env,
+            NETUTILS_PKG_NAME, gNetworkUtilMethods, NELEM(gNetworkUtilMethods));
+}
+
+}; // namespace android
diff --git a/core/tests/coretests/res/raw/xt_qtaguid_iface_fmt_typical b/core/tests/coretests/res/raw/xt_qtaguid_iface_fmt_typical
new file mode 100644
index 0000000..656d5bb
--- /dev/null
+++ b/core/tests/coretests/res/raw/xt_qtaguid_iface_fmt_typical
@@ -0,0 +1,4 @@
+ifname total_skb_rx_bytes total_skb_rx_packets total_skb_tx_bytes total_skb_tx_packets
+rmnet2 4968 35 3081 39
+rmnet1 11153922 8051 190226 2468
+rmnet0 6824 16 5692 10
diff --git a/core/tests/coretests/res/raw/xt_qtaguid_iface_typical b/core/tests/coretests/res/raw/xt_qtaguid_iface_typical
new file mode 100644
index 0000000..610723a
--- /dev/null
+++ b/core/tests/coretests/res/raw/xt_qtaguid_iface_typical
@@ -0,0 +1,6 @@
+rmnet3 1 0 0 0 0 20822 501 1149991 815
+rmnet2 1 0 0 0 0 1594 15 1313 15
+rmnet1 1 0 0 0 0 207398 458 166918 565
+rmnet0 1 0 0 0 0 2112 24 700 10
+test1 1 1 2 3 4 5 6 7 8
+test2 0 1 2 3 4 5 6 7 8
diff --git a/core/tests/coretests/res/raw/xt_qtaguid_typical b/core/tests/coretests/res/raw/xt_qtaguid_typical
new file mode 100644
index 0000000..c1b0d25
--- /dev/null
+++ b/core/tests/coretests/res/raw/xt_qtaguid_typical
@@ -0,0 +1,71 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 wlan0 0x0 0 0 18621 96 2898 44 312 6 15897 58 2412 32 312 6 1010 16 1576 22
+3 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+4 wlan0 0x0 1000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+5 wlan0 0x0 1000 1 1949 13 1078 14 0 0 1600 10 349 3 0 0 600 10 478 4
+6 wlan0 0x0 10005 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+7 wlan0 0x0 10005 1 32081 38 5315 50 32081 38 0 0 0 0 5315 50 0 0 0 0
+8 wlan0 0x0 10011 0 35777 53 5718 57 0 0 0 0 35777 53 0 0 0 0 5718 57
+9 wlan0 0x0 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+10 wlan0 0x0 10014 0 0 0 1098 13 0 0 0 0 0 0 0 0 0 0 1098 13
+11 wlan0 0x0 10014 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+12 wlan0 0x0 10021 0 562386 573 49228 549 0 0 0 0 562386 573 0 0 0 0 49228 549
+13 wlan0 0x0 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+14 wlan0 0x0 10031 0 3425 5 586 6 0 0 0 0 3425 5 0 0 0 0 586 6
+15 wlan0 0x0 10031 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+16 wlan0 0x7fffff0100000000 10021 0 562386 573 49228 549 0 0 0 0 562386 573 0 0 0 0 49228 549
+17 wlan0 0x7fffff0100000000 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+18 wlan0 0x7fffff0100000000 10031 0 3425 5 586 6 0 0 0 0 3425 5 0 0 0 0 586 6
+19 wlan0 0x7fffff0100000000 10031 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+20 rmnet2 0x0 0 0 547 5 118 2 40 1 243 1 264 3 0 0 62 1 56 1
+21 rmnet2 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+22 rmnet2 0x0 10001 0 1125899906842624 5 984 11 632 5 0 0 0 0 984 11 0 0 0 0
+23 rmnet2 0x0 10001 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+24 rmnet1 0x0 0 0 26736 174 7098 130 7210 97 18382 64 1144 13 2932 64 4054 64 112 2
+25 rmnet1 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+26 rmnet1 0x0 1000 0 75774 77 18038 78 75335 72 439 5 0 0 17668 73 370 5 0 0
+27 rmnet1 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+28 rmnet1 0x0 10007 0 269945 578 111632 586 269945 578 0 0 0 0 111632 586 0 0 0 0
+29 rmnet1 0x0 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+30 rmnet1 0x0 10011 0 1741256 6918 769778 7019 1741256 6918 0 0 0 0 769778 7019 0 0 0 0
+31 rmnet1 0x0 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+32 rmnet1 0x0 10014 0 0 0 786 12 0 0 0 0 0 0 786 12 0 0 0 0
+33 rmnet1 0x0 10014 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+34 rmnet1 0x0 10021 0 433533 1454 393420 1604 433533 1454 0 0 0 0 393420 1604 0 0 0 0
+35 rmnet1 0x0 10021 1 21215 33 10278 33 21215 33 0 0 0 0 10278 33 0 0 0 0
+36 rmnet1 0x0 10036 0 6310 25 3284 29 6310 25 0 0 0 0 3284 29 0 0 0 0
+37 rmnet1 0x0 10036 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+38 rmnet1 0x0 10047 0 34264 47 3936 34 34264 47 0 0 0 0 3936 34 0 0 0 0
+39 rmnet1 0x0 10047 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+40 rmnet1 0x4e7700000000 10011 0 9187 27 4248 33 9187 27 0 0 0 0 4248 33 0 0 0 0
+41 rmnet1 0x4e7700000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+42 rmnet1 0x1000000000000000 10007 0 2109 4 791 4 2109 4 0 0 0 0 791 4 0 0 0 0
+43 rmnet1 0x1000000000000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+44 rmnet1 0x1000000400000000 10007 0 9811 22 6286 22 9811 22 0 0 0 0 6286 22 0 0 0 0
+45 rmnet1 0x1000000400000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+46 rmnet1 0x1010000000000000 10021 0 164833 426 135392 527 164833 426 0 0 0 0 135392 527 0 0 0 0
+47 rmnet1 0x1010000000000000 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+48 rmnet1 0x1144000400000000 10011 0 10112 18 3334 17 10112 18 0 0 0 0 3334 17 0 0 0 0
+49 rmnet1 0x1144000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+50 rmnet1 0x1244000400000000 10011 0 1300 3 848 2 1300 3 0 0 0 0 848 2 0 0 0 0
+51 rmnet1 0x1244000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+52 rmnet1 0x3000000000000000 10007 0 10389 14 1521 12 10389 14 0 0 0 0 1521 12 0 0 0 0
+53 rmnet1 0x3000000000000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+54 rmnet1 0x3000000400000000 10007 0 238070 380 93938 404 238070 380 0 0 0 0 93938 404 0 0 0 0
+55 rmnet1 0x3000000400000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+56 rmnet1 0x3010000000000000 10021 0 219110 578 227423 676 219110 578 0 0 0 0 227423 676 0 0 0 0
+57 rmnet1 0x3010000000000000 10021 1 742 3 1265 3 742 3 0 0 0 0 1265 3 0 0 0 0
+58 rmnet1 0x3020000000000000 10021 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+59 rmnet1 0x3020000000000000 10021 1 20473 30 9013 30 20473 30 0 0 0 0 9013 30 0 0 0 0
+60 rmnet1 0x3144000400000000 10011 0 43963 92 34414 116 43963 92 0 0 0 0 34414 116 0 0 0 0
+61 rmnet1 0x3144000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+62 rmnet1 0x3244000400000000 10011 0 3486 8 1520 9 3486 8 0 0 0 0 1520 9 0 0 0 0
+63 rmnet1 0x3244000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+64 rmnet1 0x7fffff0100000000 10021 0 29102 56 8865 60 29102 56 0 0 0 0 8865 60 0 0 0 0
+65 rmnet1 0x7fffff0100000000 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+66 rmnet1 0x7fffff0300000000 1000 0 995 13 14145 14 995 13 0 0 0 0 14145 14 0 0 0 0
+67 rmnet1 0x7fffff0300000000 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+68 rmnet0 0x0 0 0 4312 49 1288 23 0 0 0 0 4312 49 0 0 0 0 1288 23
+69 rmnet0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+70 rmnet0 0x0 10080 0 22266 30 20976 30 0 0 0 0 22266 30 0 0 0 0 20976 30
+71 rmnet0 0x0 10080 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
diff --git a/core/tests/coretests/src/android/net/LinkAddressTest.java b/core/tests/coretests/src/android/net/LinkAddressTest.java
new file mode 100644
index 0000000..814ecdd
--- /dev/null
+++ b/core/tests/coretests/src/android/net/LinkAddressTest.java
@@ -0,0 +1,331 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import android.net.LinkAddress;
+import android.os.Parcel;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import static android.system.OsConstants.IFA_F_DEPRECATED;
+import static android.system.OsConstants.IFA_F_PERMANENT;
+import static android.system.OsConstants.IFA_F_TENTATIVE;
+import static android.system.OsConstants.RT_SCOPE_HOST;
+import static android.system.OsConstants.RT_SCOPE_LINK;
+import static android.system.OsConstants.RT_SCOPE_SITE;
+import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
+
+/**
+ * Tests for {@link LinkAddress}.
+ */
+public class LinkAddressTest extends AndroidTestCase {
+
+    private static final String V4 = "192.0.2.1";
+    private static final String V6 = "2001:db8::1";
+    private static final InetAddress V4_ADDRESS = NetworkUtils.numericToInetAddress(V4);
+    private static final InetAddress V6_ADDRESS = NetworkUtils.numericToInetAddress(V6);
+
+    public void testConstructors() throws SocketException {
+        LinkAddress address;
+
+        // Valid addresses work as expected.
+        address = new LinkAddress(V4_ADDRESS, 25);
+        assertEquals(V4_ADDRESS, address.getAddress());
+        assertEquals(25, address.getPrefixLength());
+        assertEquals(0, address.getFlags());
+        assertEquals(RT_SCOPE_UNIVERSE, address.getScope());
+
+        address = new LinkAddress(V6_ADDRESS, 127);
+        assertEquals(V6_ADDRESS, address.getAddress());
+        assertEquals(127, address.getPrefixLength());
+        assertEquals(0, address.getFlags());
+        assertEquals(RT_SCOPE_UNIVERSE, address.getScope());
+
+        // Nonsensical flags/scopes or combinations thereof are acceptable.
+        address = new LinkAddress(V6 + "/64", IFA_F_DEPRECATED | IFA_F_PERMANENT, RT_SCOPE_LINK);
+        assertEquals(V6_ADDRESS, address.getAddress());
+        assertEquals(64, address.getPrefixLength());
+        assertEquals(IFA_F_DEPRECATED | IFA_F_PERMANENT, address.getFlags());
+        assertEquals(RT_SCOPE_LINK, address.getScope());
+
+        address = new LinkAddress(V4 + "/23", 123, 456);
+        assertEquals(V4_ADDRESS, address.getAddress());
+        assertEquals(23, address.getPrefixLength());
+        assertEquals(123, address.getFlags());
+        assertEquals(456, address.getScope());
+
+        // InterfaceAddress doesn't have a constructor. Fetch some from an interface.
+        List<InterfaceAddress> addrs = NetworkInterface.getByName("lo").getInterfaceAddresses();
+
+        // We expect to find 127.0.0.1/8 and ::1/128, in any order.
+        LinkAddress ipv4Loopback, ipv6Loopback;
+        assertEquals(2, addrs.size());
+        if (addrs.get(0).getAddress() instanceof Inet4Address) {
+            ipv4Loopback = new LinkAddress(addrs.get(0));
+            ipv6Loopback = new LinkAddress(addrs.get(1));
+        } else {
+            ipv4Loopback = new LinkAddress(addrs.get(1));
+            ipv6Loopback = new LinkAddress(addrs.get(0));
+        }
+
+        assertEquals(NetworkUtils.numericToInetAddress("127.0.0.1"), ipv4Loopback.getAddress());
+        assertEquals(8, ipv4Loopback.getPrefixLength());
+
+        assertEquals(NetworkUtils.numericToInetAddress("::1"), ipv6Loopback.getAddress());
+        assertEquals(128, ipv6Loopback.getPrefixLength());
+
+        // Null addresses are rejected.
+        try {
+            address = new LinkAddress(null, 24);
+            fail("Null InetAddress should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress((String) null, IFA_F_PERMANENT, RT_SCOPE_UNIVERSE);
+            fail("Null string should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress((InterfaceAddress) null);
+            fail("Null string should cause NullPointerException");
+        } catch(NullPointerException expected) {}
+
+        // Invalid prefix lengths are rejected.
+        try {
+            address = new LinkAddress(V4_ADDRESS, -1);
+            fail("Negative IPv4 prefix length should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress(V6_ADDRESS, -1);
+            fail("Negative IPv6 prefix length should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress(V4_ADDRESS, 33);
+            fail("/33 IPv4 prefix length should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress(V4 + "/33", IFA_F_PERMANENT, RT_SCOPE_UNIVERSE);
+            fail("/33 IPv4 prefix length should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+
+        try {
+            address = new LinkAddress(V6_ADDRESS, 129, IFA_F_PERMANENT, RT_SCOPE_UNIVERSE);
+            fail("/129 IPv6 prefix length should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress(V6 + "/129", IFA_F_PERMANENT, RT_SCOPE_UNIVERSE);
+            fail("/129 IPv6 prefix length should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        // Multicast addresses are rejected.
+        try {
+            address = new LinkAddress("224.0.0.2/32");
+            fail("IPv4 multicast address should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress("ff02::1/128");
+            fail("IPv6 multicast address should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+    }
+
+    public void testAddressScopes() {
+        assertEquals(RT_SCOPE_HOST, new LinkAddress("::/128").getScope());
+        assertEquals(RT_SCOPE_HOST, new LinkAddress("0.0.0.0/32").getScope());
+
+        assertEquals(RT_SCOPE_LINK, new LinkAddress("::1/128").getScope());
+        assertEquals(RT_SCOPE_LINK, new LinkAddress("127.0.0.5/8").getScope());
+        assertEquals(RT_SCOPE_LINK, new LinkAddress("fe80::ace:d00d/64").getScope());
+        assertEquals(RT_SCOPE_LINK, new LinkAddress("169.254.5.12/16").getScope());
+
+        assertEquals(RT_SCOPE_SITE, new LinkAddress("fec0::dead/64").getScope());
+
+        assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("10.1.2.3/21").getScope());
+        assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("192.0.2.1/25").getScope());
+        assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("2001:db8::/64").getScope());
+        assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("5000::/127").getScope());
+    }
+
+    private void assertIsSameAddressAs(LinkAddress l1, LinkAddress l2) {
+        assertTrue(l1 + " unexpectedly does not have same address as " + l2,
+                l1.isSameAddressAs(l2));
+        assertTrue(l2 + " unexpectedly does not have same address as " + l1,
+                l2.isSameAddressAs(l1));
+    }
+
+    private void assertIsNotSameAddressAs(LinkAddress l1, LinkAddress l2) {
+        assertFalse(l1 + " unexpectedly has same address as " + l2,
+                l1.isSameAddressAs(l2));
+        assertFalse(l2 + " unexpectedly has same address as " + l1,
+                l1.isSameAddressAs(l2));
+    }
+
+    private void assertLinkAddressesEqual(LinkAddress l1, LinkAddress l2) {
+        assertTrue(l1 + " unexpectedly not equal to " + l2, l1.equals(l2));
+        assertTrue(l2 + " unexpectedly not equal to " + l1, l2.equals(l1));
+        assertEquals(l1.hashCode(), l2.hashCode());
+    }
+
+    private void assertLinkAddressesNotEqual(LinkAddress l1, LinkAddress l2) {
+        assertFalse(l1 + " unexpectedly equal to " + l2, l1.equals(l2));
+        assertFalse(l2 + " unexpectedly equal to " + l1, l2.equals(l1));
+    }
+
+    public void testEqualsAndSameAddressAs() {
+        LinkAddress l1, l2, l3;
+
+        l1 = new LinkAddress("2001:db8::1/64");
+        l2 = new LinkAddress("2001:db8::1/64");
+        assertLinkAddressesEqual(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+
+        l2 = new LinkAddress("2001:db8::1/65");
+        assertLinkAddressesNotEqual(l1, l2);
+        assertIsNotSameAddressAs(l1, l2);
+
+        l2 = new LinkAddress("2001:db8::2/64");
+        assertLinkAddressesNotEqual(l1, l2);
+        assertIsNotSameAddressAs(l1, l2);
+
+
+        l1 = new LinkAddress("192.0.2.1/24");
+        l2 = new LinkAddress("192.0.2.1/24");
+        assertLinkAddressesEqual(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+
+        l2 = new LinkAddress("192.0.2.1/23");
+        assertLinkAddressesNotEqual(l1, l2);
+        assertIsNotSameAddressAs(l1, l2);
+
+        l2 = new LinkAddress("192.0.2.2/24");
+        assertLinkAddressesNotEqual(l1, l2);
+        assertIsNotSameAddressAs(l1, l2);
+
+
+        // Check equals() and isSameAddressAs() on identical addresses with different flags.
+        l1 = new LinkAddress(V6_ADDRESS, 64);
+        l2 = new LinkAddress(V6_ADDRESS, 64, 0, RT_SCOPE_UNIVERSE);
+        assertLinkAddressesEqual(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+
+        l2 = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED, RT_SCOPE_UNIVERSE);
+        assertLinkAddressesNotEqual(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+
+        // Check equals() and isSameAddressAs() on identical addresses with different scope.
+        l1 = new LinkAddress(V4_ADDRESS, 24);
+        l2 = new LinkAddress(V4_ADDRESS, 24, 0, RT_SCOPE_UNIVERSE);
+        assertLinkAddressesEqual(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+
+        l2 = new LinkAddress(V4_ADDRESS, 24, 0, RT_SCOPE_HOST);
+        assertLinkAddressesNotEqual(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+
+        // Addresses with the same start or end bytes aren't equal between families.
+        l1 = new LinkAddress("32.1.13.184/24");
+        l2 = new LinkAddress("2001:db8::1/24");
+        l3 = new LinkAddress("::2001:db8/24");
+
+        byte[] ipv4Bytes = l1.getAddress().getAddress();
+        byte[] l2FirstIPv6Bytes = Arrays.copyOf(l2.getAddress().getAddress(), 4);
+        byte[] l3LastIPv6Bytes = Arrays.copyOfRange(l3.getAddress().getAddress(), 12, 16);
+        assertTrue(Arrays.equals(ipv4Bytes, l2FirstIPv6Bytes));
+        assertTrue(Arrays.equals(ipv4Bytes, l3LastIPv6Bytes));
+
+        assertLinkAddressesNotEqual(l1, l2);
+        assertIsNotSameAddressAs(l1, l2);
+
+        assertLinkAddressesNotEqual(l1, l3);
+        assertIsNotSameAddressAs(l1, l3);
+
+        // Because we use InetAddress, an IPv4 address is equal to its IPv4-mapped address.
+        // TODO: Investigate fixing this.
+        String addressString = V4 + "/24";
+        l1 = new LinkAddress(addressString);
+        l2 = new LinkAddress("::ffff:" + addressString);
+        assertLinkAddressesEqual(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+    }
+
+    public void testHashCode() {
+        LinkAddress l;
+
+        l = new LinkAddress(V4_ADDRESS, 23);
+        assertEquals(-982787, l.hashCode());
+
+        l = new LinkAddress(V4_ADDRESS, 23, 0, RT_SCOPE_HOST);
+        assertEquals(-971865, l.hashCode());
+
+        l = new LinkAddress(V4_ADDRESS, 27);
+        assertEquals(-982743, l.hashCode());
+
+        l = new LinkAddress(V6_ADDRESS, 64);
+        assertEquals(1076522926, l.hashCode());
+
+        l = new LinkAddress(V6_ADDRESS, 128);
+        assertEquals(1076523630, l.hashCode());
+
+        l = new LinkAddress(V6_ADDRESS, 128, IFA_F_TENTATIVE, RT_SCOPE_UNIVERSE);
+        assertEquals(1076524846, l.hashCode());
+    }
+
+    private LinkAddress passThroughParcel(LinkAddress l) {
+        Parcel p = Parcel.obtain();
+        LinkAddress l2 = null;
+        try {
+            l.writeToParcel(p, 0);
+            p.setDataPosition(0);
+            l2 = LinkAddress.CREATOR.createFromParcel(p);
+        } finally {
+            p.recycle();
+        }
+        assertNotNull(l2);
+        return l2;
+    }
+
+    private void assertParcelingIsLossless(LinkAddress l) {
+      LinkAddress l2 = passThroughParcel(l);
+      assertEquals(l, l2);
+    }
+
+    public void testParceling() {
+        LinkAddress l;
+
+        l = new LinkAddress(V6_ADDRESS, 64, 123, 456);
+        assertParcelingIsLossless(l);
+
+        l = new LinkAddress(V4 + "/28", IFA_F_PERMANENT, RT_SCOPE_LINK);
+        assertParcelingIsLossless(l);
+    }
+}
diff --git a/core/tests/coretests/src/android/net/LinkPropertiesTest.java b/core/tests/coretests/src/android/net/LinkPropertiesTest.java
new file mode 100644
index 0000000..e649baa
--- /dev/null
+++ b/core/tests/coretests/src/android/net/LinkPropertiesTest.java
@@ -0,0 +1,440 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+import android.net.LinkProperties;
+import android.net.RouteInfo;
+import android.system.OsConstants;
+import android.test.suitebuilder.annotation.SmallTest;
+import junit.framework.TestCase;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+
+public class LinkPropertiesTest extends TestCase {
+    private static InetAddress ADDRV4 = NetworkUtils.numericToInetAddress("75.208.6.1");
+    private static InetAddress ADDRV6 = NetworkUtils.numericToInetAddress(
+            "2001:0db8:85a3:0000:0000:8a2e:0370:7334");
+    private static InetAddress DNS1 = NetworkUtils.numericToInetAddress("75.208.7.1");
+    private static InetAddress DNS2 = NetworkUtils.numericToInetAddress("69.78.7.1");
+    private static InetAddress GATEWAY1 = NetworkUtils.numericToInetAddress("75.208.8.1");
+    private static InetAddress GATEWAY2 = NetworkUtils.numericToInetAddress("69.78.8.1");
+    private static String NAME = "qmi0";
+    private static int MTU = 1500;
+
+    private static LinkAddress LINKADDRV4 = new LinkAddress(ADDRV4, 32);
+    private static LinkAddress LINKADDRV6 = new LinkAddress(ADDRV6, 128);
+
+    public void assertLinkPropertiesEqual(LinkProperties source, LinkProperties target) {
+        // Check implementation of equals(), element by element.
+        assertTrue(source.isIdenticalInterfaceName(target));
+        assertTrue(target.isIdenticalInterfaceName(source));
+
+        assertTrue(source.isIdenticalAddresses(target));
+        assertTrue(target.isIdenticalAddresses(source));
+
+        assertTrue(source.isIdenticalDnses(target));
+        assertTrue(target.isIdenticalDnses(source));
+
+        assertTrue(source.isIdenticalRoutes(target));
+        assertTrue(target.isIdenticalRoutes(source));
+
+        assertTrue(source.isIdenticalHttpProxy(target));
+        assertTrue(target.isIdenticalHttpProxy(source));
+
+        assertTrue(source.isIdenticalStackedLinks(target));
+        assertTrue(target.isIdenticalStackedLinks(source));
+
+        assertTrue(source.isIdenticalMtu(target));
+        assertTrue(target.isIdenticalMtu(source));
+
+        // Check result of equals().
+        assertTrue(source.equals(target));
+        assertTrue(target.equals(source));
+
+        // Check hashCode.
+        assertEquals(source.hashCode(), target.hashCode());
+    }
+
+    @SmallTest
+    public void testEqualsNull() {
+        LinkProperties source = new LinkProperties();
+        LinkProperties target = new LinkProperties();
+
+        assertFalse(source == target);
+        assertLinkPropertiesEqual(source, target);
+    }
+
+    @SmallTest
+    public void testEqualsSameOrder() {
+        try {
+            LinkProperties source = new LinkProperties();
+            source.setInterfaceName(NAME);
+            // set 2 link addresses
+            source.addLinkAddress(LINKADDRV4);
+            source.addLinkAddress(LINKADDRV6);
+            // set 2 dnses
+            source.addDnsServer(DNS1);
+            source.addDnsServer(DNS2);
+            // set 2 gateways
+            source.addRoute(new RouteInfo(GATEWAY1));
+            source.addRoute(new RouteInfo(GATEWAY2));
+            source.setMtu(MTU);
+
+            LinkProperties target = new LinkProperties();
+
+            // All fields are same
+            target.setInterfaceName(NAME);
+            target.addLinkAddress(LINKADDRV4);
+            target.addLinkAddress(LINKADDRV6);
+            target.addDnsServer(DNS1);
+            target.addDnsServer(DNS2);
+            target.addRoute(new RouteInfo(GATEWAY1));
+            target.addRoute(new RouteInfo(GATEWAY2));
+            target.setMtu(MTU);
+
+            assertLinkPropertiesEqual(source, target);
+
+            target.clear();
+            // change Interface Name
+            target.setInterfaceName("qmi1");
+            target.addLinkAddress(LINKADDRV4);
+            target.addLinkAddress(LINKADDRV6);
+            target.addDnsServer(DNS1);
+            target.addDnsServer(DNS2);
+            target.addRoute(new RouteInfo(GATEWAY1));
+            target.addRoute(new RouteInfo(GATEWAY2));
+            target.setMtu(MTU);
+            assertFalse(source.equals(target));
+
+            target.clear();
+            target.setInterfaceName(NAME);
+            // change link addresses
+            target.addLinkAddress(new LinkAddress(
+                    NetworkUtils.numericToInetAddress("75.208.6.2"), 32));
+            target.addLinkAddress(LINKADDRV6);
+            target.addDnsServer(DNS1);
+            target.addDnsServer(DNS2);
+            target.addRoute(new RouteInfo(GATEWAY1));
+            target.addRoute(new RouteInfo(GATEWAY2));
+            target.setMtu(MTU);
+            assertFalse(source.equals(target));
+
+            target.clear();
+            target.setInterfaceName(NAME);
+            target.addLinkAddress(LINKADDRV4);
+            target.addLinkAddress(LINKADDRV6);
+            // change dnses
+            target.addDnsServer(NetworkUtils.numericToInetAddress("75.208.7.2"));
+            target.addDnsServer(DNS2);
+            target.addRoute(new RouteInfo(GATEWAY1));
+            target.addRoute(new RouteInfo(GATEWAY2));
+            target.setMtu(MTU);
+            assertFalse(source.equals(target));
+
+            target.clear();
+            target.setInterfaceName(NAME);
+            target.addLinkAddress(LINKADDRV4);
+            target.addLinkAddress(LINKADDRV6);
+            target.addDnsServer(DNS1);
+            target.addDnsServer(DNS2);
+            // change gateway
+            target.addRoute(new RouteInfo(NetworkUtils.numericToInetAddress("75.208.8.2")));
+            target.addRoute(new RouteInfo(GATEWAY2));
+            target.setMtu(MTU);
+            assertFalse(source.equals(target));
+
+            target.clear();
+            target.setInterfaceName(NAME);
+            target.addLinkAddress(LINKADDRV4);
+            target.addLinkAddress(LINKADDRV6);
+            target.addDnsServer(DNS1);
+            target.addDnsServer(DNS2);
+            target.addRoute(new RouteInfo(GATEWAY1));
+            target.addRoute(new RouteInfo(GATEWAY2));
+            // change mtu
+            target.setMtu(1440);
+            assertFalse(source.equals(target));
+
+        } catch (Exception e) {
+            throw new RuntimeException(e.toString());
+            //fail();
+        }
+    }
+
+    @SmallTest
+    public void testEqualsDifferentOrder() {
+        try {
+            LinkProperties source = new LinkProperties();
+            source.setInterfaceName(NAME);
+            // set 2 link addresses
+            source.addLinkAddress(LINKADDRV4);
+            source.addLinkAddress(LINKADDRV6);
+            // set 2 dnses
+            source.addDnsServer(DNS1);
+            source.addDnsServer(DNS2);
+            // set 2 gateways
+            source.addRoute(new RouteInfo(GATEWAY1));
+            source.addRoute(new RouteInfo(GATEWAY2));
+            source.setMtu(MTU);
+
+            LinkProperties target = new LinkProperties();
+            // Exchange order
+            target.setInterfaceName(NAME);
+            target.addLinkAddress(LINKADDRV6);
+            target.addLinkAddress(LINKADDRV4);
+            target.addDnsServer(DNS2);
+            target.addDnsServer(DNS1);
+            target.addRoute(new RouteInfo(GATEWAY2));
+            target.addRoute(new RouteInfo(GATEWAY1));
+            target.setMtu(MTU);
+
+            assertLinkPropertiesEqual(source, target);
+        } catch (Exception e) {
+            fail();
+        }
+    }
+
+    @SmallTest
+    public void testEqualsDuplicated() {
+        try {
+            LinkProperties source = new LinkProperties();
+            // set 3 link addresses, eg, [A, A, B]
+            source.addLinkAddress(LINKADDRV4);
+            source.addLinkAddress(LINKADDRV4);
+            source.addLinkAddress(LINKADDRV6);
+
+            LinkProperties target = new LinkProperties();
+            // set 3 link addresses, eg, [A, B, B]
+            target.addLinkAddress(LINKADDRV4);
+            target.addLinkAddress(LINKADDRV6);
+            target.addLinkAddress(LINKADDRV6);
+
+            assertLinkPropertiesEqual(source, target);
+        } catch (Exception e) {
+            fail();
+        }
+    }
+
+    private void assertAllRoutesHaveInterface(String iface, LinkProperties lp) {
+        for (RouteInfo r : lp.getRoutes()) {
+            assertEquals(iface, r.getInterface());
+        }
+    }
+
+    @SmallTest
+    public void testRouteInterfaces() {
+        LinkAddress prefix = new LinkAddress(
+            NetworkUtils.numericToInetAddress("2001:db8::"), 32);
+        InetAddress address = ADDRV6;
+
+        // Add a route with no interface to a LinkProperties with no interface. No errors.
+        LinkProperties lp = new LinkProperties();
+        RouteInfo r = new RouteInfo(prefix, address, null);
+        lp.addRoute(r);
+        assertEquals(1, lp.getRoutes().size());
+        assertAllRoutesHaveInterface(null, lp);
+
+        // Add a route with an interface. Except an exception.
+        r = new RouteInfo(prefix, address, "wlan0");
+        try {
+          lp.addRoute(r);
+          fail("Adding wlan0 route to LP with no interface, expect exception");
+        } catch (IllegalArgumentException expected) {}
+
+        // Change the interface name. All the routes should change their interface name too.
+        lp.setInterfaceName("rmnet0");
+        assertAllRoutesHaveInterface("rmnet0", lp);
+
+        // Now add a route with the wrong interface. This causes an exception too.
+        try {
+          lp.addRoute(r);
+          fail("Adding wlan0 route to rmnet0 LP, expect exception");
+        } catch (IllegalArgumentException expected) {}
+
+        // If the interface name matches, the route is added.
+        lp.setInterfaceName("wlan0");
+        lp.addRoute(r);
+        assertEquals(2, lp.getRoutes().size());
+        assertAllRoutesHaveInterface("wlan0", lp);
+
+        // Routes with null interfaces are converted to wlan0.
+        r = RouteInfo.makeHostRoute(ADDRV6, null);
+        lp.addRoute(r);
+        assertEquals(3, lp.getRoutes().size());
+        assertAllRoutesHaveInterface("wlan0", lp);
+
+        // Check comparisons work.
+        LinkProperties lp2 = new LinkProperties(lp);
+        assertAllRoutesHaveInterface("wlan0", lp);
+        assertEquals(0, lp.compareAllRoutes(lp2).added.size());
+        assertEquals(0, lp.compareAllRoutes(lp2).removed.size());
+
+        lp2.setInterfaceName("p2p0");
+        assertAllRoutesHaveInterface("p2p0", lp2);
+        assertEquals(3, lp.compareAllRoutes(lp2).added.size());
+        assertEquals(3, lp.compareAllRoutes(lp2).removed.size());
+    }
+
+    @SmallTest
+    public void testStackedInterfaces() {
+        LinkProperties rmnet0 = new LinkProperties();
+        rmnet0.setInterfaceName("rmnet0");
+        rmnet0.addLinkAddress(LINKADDRV6);
+
+        LinkProperties clat4 = new LinkProperties();
+        clat4.setInterfaceName("clat4");
+        clat4.addLinkAddress(LINKADDRV4);
+
+        assertEquals(0, rmnet0.getStackedLinks().size());
+        assertEquals(1, rmnet0.getAddresses().size());
+        assertEquals(1, rmnet0.getLinkAddresses().size());
+        assertEquals(1, rmnet0.getAllAddresses().size());
+        assertEquals(1, rmnet0.getAllLinkAddresses().size());
+
+        rmnet0.addStackedLink(clat4);
+        assertEquals(1, rmnet0.getStackedLinks().size());
+        assertEquals(1, rmnet0.getAddresses().size());
+        assertEquals(1, rmnet0.getLinkAddresses().size());
+        assertEquals(2, rmnet0.getAllAddresses().size());
+        assertEquals(2, rmnet0.getAllLinkAddresses().size());
+
+        rmnet0.addStackedLink(clat4);
+        assertEquals(1, rmnet0.getStackedLinks().size());
+        assertEquals(1, rmnet0.getAddresses().size());
+        assertEquals(1, rmnet0.getLinkAddresses().size());
+        assertEquals(2, rmnet0.getAllAddresses().size());
+        assertEquals(2, rmnet0.getAllLinkAddresses().size());
+
+        assertEquals(0, clat4.getStackedLinks().size());
+
+        // Modify an item in the returned collection to see what happens.
+        for (LinkProperties link : rmnet0.getStackedLinks()) {
+            if (link.getInterfaceName().equals("clat4")) {
+               link.setInterfaceName("newname");
+            }
+        }
+        for (LinkProperties link : rmnet0.getStackedLinks()) {
+            assertFalse("newname".equals(link.getInterfaceName()));
+        }
+
+        assertTrue(rmnet0.removeStackedLink(clat4));
+        assertEquals(0, rmnet0.getStackedLinks().size());
+        assertEquals(1, rmnet0.getAddresses().size());
+        assertEquals(1, rmnet0.getLinkAddresses().size());
+        assertEquals(1, rmnet0.getAllAddresses().size());
+        assertEquals(1, rmnet0.getAllLinkAddresses().size());
+
+        assertFalse(rmnet0.removeStackedLink(clat4));
+    }
+
+    private LinkAddress getFirstLinkAddress(LinkProperties lp) {
+        return lp.getLinkAddresses().iterator().next();
+    }
+
+    @SmallTest
+    public void testAddressMethods() {
+        LinkProperties lp = new LinkProperties();
+
+        // No addresses.
+        assertFalse(lp.hasIPv4Address());
+        assertFalse(lp.hasIPv6Address());
+
+        // Addresses on stacked links don't count.
+        LinkProperties stacked = new LinkProperties();
+        stacked.setInterfaceName("stacked");
+        lp.addStackedLink(stacked);
+        stacked.addLinkAddress(LINKADDRV4);
+        stacked.addLinkAddress(LINKADDRV6);
+        assertTrue(stacked.hasIPv4Address());
+        assertTrue(stacked.hasIPv6Address());
+        assertFalse(lp.hasIPv4Address());
+        assertFalse(lp.hasIPv6Address());
+        lp.removeStackedLink(stacked);
+        assertFalse(lp.hasIPv4Address());
+        assertFalse(lp.hasIPv6Address());
+
+        // Addresses on the base link.
+        // Check the return values of hasIPvXAddress and ensure the add/remove methods return true
+        // iff something changes.
+        assertEquals(0, lp.getLinkAddresses().size());
+        assertTrue(lp.addLinkAddress(LINKADDRV6));
+        assertEquals(1, lp.getLinkAddresses().size());
+        assertFalse(lp.hasIPv4Address());
+        assertTrue(lp.hasIPv6Address());
+
+        assertTrue(lp.removeLinkAddress(LINKADDRV6));
+        assertEquals(0, lp.getLinkAddresses().size());
+        assertTrue(lp.addLinkAddress(LINKADDRV4));
+        assertEquals(1, lp.getLinkAddresses().size());
+        assertTrue(lp.hasIPv4Address());
+        assertFalse(lp.hasIPv6Address());
+
+        assertTrue(lp.addLinkAddress(LINKADDRV6));
+        assertEquals(2, lp.getLinkAddresses().size());
+        assertTrue(lp.hasIPv4Address());
+        assertTrue(lp.hasIPv6Address());
+
+        // Adding an address twice has no effect.
+        // Removing an address that's not present has no effect.
+        assertFalse(lp.addLinkAddress(LINKADDRV4));
+        assertEquals(2, lp.getLinkAddresses().size());
+        assertTrue(lp.hasIPv4Address());
+        assertTrue(lp.removeLinkAddress(LINKADDRV4));
+        assertEquals(1, lp.getLinkAddresses().size());
+        assertFalse(lp.hasIPv4Address());
+        assertFalse(lp.removeLinkAddress(LINKADDRV4));
+        assertEquals(1, lp.getLinkAddresses().size());
+
+        // Adding an address that's already present but with different properties causes the
+        // existing address to be updated and returns true.
+        // Start with only LINKADDRV6.
+        assertEquals(1, lp.getLinkAddresses().size());
+        assertEquals(LINKADDRV6, getFirstLinkAddress(lp));
+
+        // Create a LinkAddress object for the same address, but with different flags.
+        LinkAddress deprecated = new LinkAddress(ADDRV6, 128,
+                OsConstants.IFA_F_DEPRECATED, OsConstants.RT_SCOPE_UNIVERSE);
+        assertTrue(deprecated.isSameAddressAs(LINKADDRV6));
+        assertFalse(deprecated.equals(LINKADDRV6));
+
+        // Check that adding it updates the existing address instead of adding a new one.
+        assertTrue(lp.addLinkAddress(deprecated));
+        assertEquals(1, lp.getLinkAddresses().size());
+        assertEquals(deprecated, getFirstLinkAddress(lp));
+        assertFalse(LINKADDRV6.equals(getFirstLinkAddress(lp)));
+
+        // Removing LINKADDRV6 removes deprecated, because removing addresses ignores properties.
+        assertTrue(lp.removeLinkAddress(LINKADDRV6));
+        assertEquals(0, lp.getLinkAddresses().size());
+    }
+
+    @SmallTest
+    public void testSetLinkAddresses() {
+        LinkProperties lp = new LinkProperties();
+        lp.addLinkAddress(LINKADDRV4);
+        lp.addLinkAddress(LINKADDRV6);
+
+        LinkProperties lp2 = new LinkProperties();
+        lp2.addLinkAddress(LINKADDRV6);
+
+        assertFalse(lp.equals(lp2));
+
+        lp2.setLinkAddresses(lp.getLinkAddresses());
+        assertTrue(lp.equals(lp));
+    }
+}
diff --git a/core/tests/coretests/src/android/net/NetworkStatsHistoryTest.java b/core/tests/coretests/src/android/net/NetworkStatsHistoryTest.java
new file mode 100644
index 0000000..b181122
--- /dev/null
+++ b/core/tests/coretests/src/android/net/NetworkStatsHistoryTest.java
@@ -0,0 +1,521 @@
+/*
+ * 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.NetworkStatsHistory.FIELD_ALL;
+import static android.net.NetworkStatsHistory.FIELD_OPERATIONS;
+import static android.net.NetworkStatsHistory.FIELD_RX_BYTES;
+import static android.net.NetworkStatsHistory.FIELD_RX_PACKETS;
+import static android.net.NetworkStatsHistory.FIELD_TX_BYTES;
+import static android.net.NetworkStatsHistory.DataStreamUtils.readVarLong;
+import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLong;
+import static android.net.NetworkStatsHistory.Entry.UNKNOWN;
+import static android.net.TrafficStats.GB_IN_BYTES;
+import static android.net.TrafficStats.MB_IN_BYTES;
+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 android.text.format.DateUtils.WEEK_IN_MILLIS;
+import static android.text.format.DateUtils.YEAR_IN_MILLIS;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.util.Log;
+
+import com.android.frameworks.coretests.R;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.util.Random;
+
+@SmallTest
+public class NetworkStatsHistoryTest extends AndroidTestCase {
+    private static final String TAG = "NetworkStatsHistoryTest";
+
+    private static final long TEST_START = 1194220800000L;
+
+    private NetworkStatsHistory stats;
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        if (stats != null) {
+            assertConsistent(stats);
+        }
+    }
+
+    public void testReadOriginalVersion() throws Exception {
+        final DataInputStream in = new DataInputStream(
+                getContext().getResources().openRawResource(R.raw.history_v1));
+
+        NetworkStatsHistory.Entry entry = null;
+        try {
+            final NetworkStatsHistory history = new NetworkStatsHistory(in);
+            assertEquals(15 * SECOND_IN_MILLIS, history.getBucketDuration());
+
+            entry = history.getValues(0, entry);
+            assertEquals(29143L, entry.rxBytes);
+            assertEquals(6223L, entry.txBytes);
+
+            entry = history.getValues(history.size() - 1, entry);
+            assertEquals(1476L, entry.rxBytes);
+            assertEquals(838L, entry.txBytes);
+
+            entry = history.getValues(Long.MIN_VALUE, Long.MAX_VALUE, entry);
+            assertEquals(332401L, entry.rxBytes);
+            assertEquals(64314L, entry.txBytes);
+
+        } finally {
+            in.close();
+        }
+    }
+
+    public void testRecordSingleBucket() throws Exception {
+        final long BUCKET_SIZE = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        // record data into narrow window to get single bucket
+        stats.recordData(TEST_START, TEST_START + SECOND_IN_MILLIS,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+
+        assertEquals(1, stats.size());
+        assertValues(stats, 0, SECOND_IN_MILLIS, 1024L, 10L, 2048L, 20L, 2L);
+    }
+
+    public void testRecordEqualBuckets() throws Exception {
+        final long bucketDuration = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(bucketDuration);
+
+        // split equally across two buckets
+        final long recordStart = TEST_START + (bucketDuration / 2);
+        stats.recordData(recordStart, recordStart + bucketDuration,
+                new NetworkStats.Entry(1024L, 10L, 128L, 2L, 2L));
+
+        assertEquals(2, stats.size());
+        assertValues(stats, 0, HOUR_IN_MILLIS / 2, 512L, 5L, 64L, 1L, 1L);
+        assertValues(stats, 1, HOUR_IN_MILLIS / 2, 512L, 5L, 64L, 1L, 1L);
+    }
+
+    public void testRecordTouchingBuckets() throws Exception {
+        final long BUCKET_SIZE = 15 * MINUTE_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        // split almost completely into middle bucket, but with a few minutes
+        // overlap into neighboring buckets. total record is 20 minutes.
+        final long recordStart = (TEST_START + BUCKET_SIZE) - MINUTE_IN_MILLIS;
+        final long recordEnd = (TEST_START + (BUCKET_SIZE * 2)) + (MINUTE_IN_MILLIS * 4);
+        stats.recordData(recordStart, recordEnd,
+                new NetworkStats.Entry(1000L, 2000L, 5000L, 10000L, 100L));
+
+        assertEquals(3, stats.size());
+        // first bucket should have (1/20 of value)
+        assertValues(stats, 0, MINUTE_IN_MILLIS, 50L, 100L, 250L, 500L, 5L);
+        // second bucket should have (15/20 of value)
+        assertValues(stats, 1, 15 * MINUTE_IN_MILLIS, 750L, 1500L, 3750L, 7500L, 75L);
+        // final bucket should have (4/20 of value)
+        assertValues(stats, 2, 4 * MINUTE_IN_MILLIS, 200L, 400L, 1000L, 2000L, 20L);
+    }
+
+    public void testRecordGapBuckets() throws Exception {
+        final long BUCKET_SIZE = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        // record some data today and next week with large gap
+        final long firstStart = TEST_START;
+        final long lastStart = TEST_START + WEEK_IN_MILLIS;
+        stats.recordData(firstStart, firstStart + SECOND_IN_MILLIS,
+                new NetworkStats.Entry(128L, 2L, 256L, 4L, 1L));
+        stats.recordData(lastStart, lastStart + SECOND_IN_MILLIS,
+                new NetworkStats.Entry(64L, 1L, 512L, 8L, 2L));
+
+        // we should have two buckets, far apart from each other
+        assertEquals(2, stats.size());
+        assertValues(stats, 0, SECOND_IN_MILLIS, 128L, 2L, 256L, 4L, 1L);
+        assertValues(stats, 1, SECOND_IN_MILLIS, 64L, 1L, 512L, 8L, 2L);
+
+        // now record something in middle, spread across two buckets
+        final long middleStart = TEST_START + DAY_IN_MILLIS;
+        final long middleEnd = middleStart + (HOUR_IN_MILLIS * 2);
+        stats.recordData(middleStart, middleEnd,
+                new NetworkStats.Entry(2048L, 4L, 2048L, 4L, 2L));
+
+        // now should have four buckets, with new record in middle two buckets
+        assertEquals(4, stats.size());
+        assertValues(stats, 0, SECOND_IN_MILLIS, 128L, 2L, 256L, 4L, 1L);
+        assertValues(stats, 1, HOUR_IN_MILLIS, 1024L, 2L, 1024L, 2L, 1L);
+        assertValues(stats, 2, HOUR_IN_MILLIS, 1024L, 2L, 1024L, 2L, 1L);
+        assertValues(stats, 3, SECOND_IN_MILLIS, 64L, 1L, 512L, 8L, 2L);
+    }
+
+    public void testRecordOverlapBuckets() throws Exception {
+        final long BUCKET_SIZE = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        // record some data in one bucket, and another overlapping buckets
+        stats.recordData(TEST_START, TEST_START + SECOND_IN_MILLIS,
+                new NetworkStats.Entry(256L, 2L, 256L, 2L, 1L));
+        final long midStart = TEST_START + (HOUR_IN_MILLIS / 2);
+        stats.recordData(midStart, midStart + HOUR_IN_MILLIS,
+                new NetworkStats.Entry(1024L, 10L, 1024L, 10L, 10L));
+
+        // should have two buckets, with some data mixed together
+        assertEquals(2, stats.size());
+        assertValues(stats, 0, SECOND_IN_MILLIS + (HOUR_IN_MILLIS / 2), 768L, 7L, 768L, 7L, 6L);
+        assertValues(stats, 1, (HOUR_IN_MILLIS / 2), 512L, 5L, 512L, 5L, 5L);
+    }
+
+    public void testRecordEntireGapIdentical() throws Exception {
+        // first, create two separate histories far apart
+        final NetworkStatsHistory stats1 = new NetworkStatsHistory(HOUR_IN_MILLIS);
+        stats1.recordData(TEST_START, TEST_START + 2 * HOUR_IN_MILLIS, 2000L, 1000L);
+
+        final long TEST_START_2 = TEST_START + DAY_IN_MILLIS;
+        final NetworkStatsHistory stats2 = new NetworkStatsHistory(HOUR_IN_MILLIS);
+        stats2.recordData(TEST_START_2, TEST_START_2 + 2 * HOUR_IN_MILLIS, 1000L, 500L);
+
+        // combine together with identical bucket size
+        stats = new NetworkStatsHistory(HOUR_IN_MILLIS);
+        stats.recordEntireHistory(stats1);
+        stats.recordEntireHistory(stats2);
+
+        // first verify that totals match up
+        assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 3000L, 1500L);
+
+        // now inspect internal buckets
+        assertValues(stats, 0, 1000L, 500L);
+        assertValues(stats, 1, 1000L, 500L);
+        assertValues(stats, 2, 500L, 250L);
+        assertValues(stats, 3, 500L, 250L);
+    }
+
+    public void testRecordEntireOverlapVaryingBuckets() throws Exception {
+        // create history just over hour bucket boundary
+        final NetworkStatsHistory stats1 = new NetworkStatsHistory(HOUR_IN_MILLIS);
+        stats1.recordData(TEST_START, TEST_START + MINUTE_IN_MILLIS * 60, 600L, 600L);
+
+        final long TEST_START_2 = TEST_START + MINUTE_IN_MILLIS;
+        final NetworkStatsHistory stats2 = new NetworkStatsHistory(MINUTE_IN_MILLIS);
+        stats2.recordData(TEST_START_2, TEST_START_2 + MINUTE_IN_MILLIS * 5, 50L, 50L);
+
+        // combine together with minute bucket size
+        stats = new NetworkStatsHistory(MINUTE_IN_MILLIS);
+        stats.recordEntireHistory(stats1);
+        stats.recordEntireHistory(stats2);
+
+        // first verify that totals match up
+        assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 650L, 650L);
+
+        // now inspect internal buckets
+        assertValues(stats, 0, 10L, 10L);
+        assertValues(stats, 1, 20L, 20L);
+        assertValues(stats, 2, 20L, 20L);
+        assertValues(stats, 3, 20L, 20L);
+        assertValues(stats, 4, 20L, 20L);
+        assertValues(stats, 5, 20L, 20L);
+        assertValues(stats, 6, 10L, 10L);
+
+        // now combine using 15min buckets
+        stats = new NetworkStatsHistory(HOUR_IN_MILLIS / 4);
+        stats.recordEntireHistory(stats1);
+        stats.recordEntireHistory(stats2);
+
+        // first verify that totals match up
+        assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 650L, 650L);
+
+        // and inspect buckets
+        assertValues(stats, 0, 200L, 200L);
+        assertValues(stats, 1, 150L, 150L);
+        assertValues(stats, 2, 150L, 150L);
+        assertValues(stats, 3, 150L, 150L);
+    }
+
+    public void testRemove() throws Exception {
+        stats = new NetworkStatsHistory(HOUR_IN_MILLIS);
+
+        // record some data across 24 buckets
+        stats.recordData(TEST_START, TEST_START + DAY_IN_MILLIS, 24L, 24L);
+        assertEquals(24, stats.size());
+
+        // try removing invalid data; should be no change
+        stats.removeBucketsBefore(0 - DAY_IN_MILLIS);
+        assertEquals(24, stats.size());
+
+        // try removing far before buckets; should be no change
+        stats.removeBucketsBefore(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);
+        assertEquals(24, stats.size());
+
+        // try removing single bucket
+        stats.removeBucketsBefore(TEST_START + HOUR_IN_MILLIS);
+        assertEquals(23, stats.size());
+
+        // try removing multiple buckets
+        stats.removeBucketsBefore(TEST_START + (4 * HOUR_IN_MILLIS));
+        assertEquals(20, stats.size());
+
+        // try removing all buckets
+        stats.removeBucketsBefore(TEST_START + YEAR_IN_MILLIS);
+        assertEquals(0, stats.size());
+    }
+
+    public void testTotalData() throws Exception {
+        final long BUCKET_SIZE = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        // record uniform data across day
+        stats.recordData(TEST_START, TEST_START + DAY_IN_MILLIS, 2400L, 4800L);
+
+        // verify that total outside range is 0
+        assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START - DAY_IN_MILLIS, 0L, 0L);
+
+        // verify total in first hour
+        assertValues(stats, TEST_START, TEST_START + HOUR_IN_MILLIS, 100L, 200L);
+
+        // verify total across 1.5 hours
+        assertValues(stats, TEST_START, TEST_START + (long) (1.5 * HOUR_IN_MILLIS), 150L, 300L);
+
+        // verify total beyond end
+        assertValues(stats, TEST_START + (23 * HOUR_IN_MILLIS), TEST_START + WEEK_IN_MILLIS, 100L, 200L);
+
+        // verify everything total
+        assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 2400L, 4800L);
+
+    }
+
+    @Suppress
+    public void testFuzzing() throws Exception {
+        try {
+            // fuzzing with random events, looking for crashes
+            final NetworkStats.Entry entry = new NetworkStats.Entry();
+            final Random r = new Random();
+            for (int i = 0; i < 500; i++) {
+                stats = new NetworkStatsHistory(r.nextLong());
+                for (int j = 0; j < 10000; j++) {
+                    if (r.nextBoolean()) {
+                        // add range
+                        final long start = r.nextLong();
+                        final long end = start + r.nextInt();
+                        entry.rxBytes = nextPositiveLong(r);
+                        entry.rxPackets = nextPositiveLong(r);
+                        entry.txBytes = nextPositiveLong(r);
+                        entry.txPackets = nextPositiveLong(r);
+                        entry.operations = nextPositiveLong(r);
+                        stats.recordData(start, end, entry);
+                    } else {
+                        // trim something
+                        stats.removeBucketsBefore(r.nextLong());
+                    }
+                }
+                assertConsistent(stats);
+            }
+        } catch (Throwable e) {
+            Log.e(TAG, String.valueOf(stats));
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static long nextPositiveLong(Random r) {
+        final long value = r.nextLong();
+        return value < 0 ? -value : value;
+    }
+
+    public void testIgnoreFields() throws Exception {
+        final NetworkStatsHistory history = new NetworkStatsHistory(
+                MINUTE_IN_MILLIS, 0, FIELD_RX_BYTES | FIELD_TX_BYTES);
+
+        history.recordData(0, MINUTE_IN_MILLIS,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L));
+        history.recordData(0, 2 * MINUTE_IN_MILLIS,
+                new NetworkStats.Entry(2L, 2L, 2L, 2L, 2L));
+
+        assertFullValues(history, UNKNOWN, 1026L, UNKNOWN, 2050L, UNKNOWN, UNKNOWN);
+    }
+
+    public void testIgnoreFieldsRecordIn() throws Exception {
+        final NetworkStatsHistory full = new NetworkStatsHistory(MINUTE_IN_MILLIS, 0, FIELD_ALL);
+        final NetworkStatsHistory partial = new NetworkStatsHistory(
+                MINUTE_IN_MILLIS, 0, FIELD_RX_PACKETS | FIELD_OPERATIONS);
+
+        full.recordData(0, MINUTE_IN_MILLIS,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L));
+        partial.recordEntireHistory(full);
+
+        assertFullValues(partial, UNKNOWN, UNKNOWN, 10L, UNKNOWN, UNKNOWN, 4L);
+    }
+
+    public void testIgnoreFieldsRecordOut() throws Exception {
+        final NetworkStatsHistory full = new NetworkStatsHistory(MINUTE_IN_MILLIS, 0, FIELD_ALL);
+        final NetworkStatsHistory partial = new NetworkStatsHistory(
+                MINUTE_IN_MILLIS, 0, FIELD_RX_PACKETS | FIELD_OPERATIONS);
+
+        partial.recordData(0, MINUTE_IN_MILLIS,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L));
+        full.recordEntireHistory(partial);
+
+        assertFullValues(full, MINUTE_IN_MILLIS, 0L, 10L, 0L, 0L, 4L);
+    }
+
+    public void testSerialize() throws Exception {
+        final NetworkStatsHistory before = new NetworkStatsHistory(MINUTE_IN_MILLIS, 40, FIELD_ALL);
+        before.recordData(0, 4 * MINUTE_IN_MILLIS,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L));
+        before.recordData(DAY_IN_MILLIS, DAY_IN_MILLIS + MINUTE_IN_MILLIS,
+                new NetworkStats.Entry(10L, 20L, 30L, 40L, 50L));
+
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        before.writeToStream(new DataOutputStream(out));
+        out.close();
+
+        final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+        final NetworkStatsHistory after = new NetworkStatsHistory(new DataInputStream(in));
+
+        // must have identical totals before and after
+        assertFullValues(before, 5 * MINUTE_IN_MILLIS, 1034L, 30L, 2078L, 60L, 54L);
+        assertFullValues(after, 5 * MINUTE_IN_MILLIS, 1034L, 30L, 2078L, 60L, 54L);
+    }
+
+    public void testVarLong() throws Exception {
+        assertEquals(0L, performVarLong(0L));
+        assertEquals(-1L, performVarLong(-1L));
+        assertEquals(1024L, performVarLong(1024L));
+        assertEquals(-1024L, performVarLong(-1024L));
+        assertEquals(40 * MB_IN_BYTES, performVarLong(40 * MB_IN_BYTES));
+        assertEquals(512 * GB_IN_BYTES, performVarLong(512 * GB_IN_BYTES));
+        assertEquals(Long.MIN_VALUE, performVarLong(Long.MIN_VALUE));
+        assertEquals(Long.MAX_VALUE, performVarLong(Long.MAX_VALUE));
+        assertEquals(Long.MIN_VALUE + 40, performVarLong(Long.MIN_VALUE + 40));
+        assertEquals(Long.MAX_VALUE - 40, performVarLong(Long.MAX_VALUE - 40));
+    }
+
+    public void testIndexBeforeAfter() throws Exception {
+        final long BUCKET_SIZE = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        final long FIRST_START = TEST_START;
+        final long FIRST_END = FIRST_START + (2 * HOUR_IN_MILLIS);
+        final long SECOND_START = TEST_START + WEEK_IN_MILLIS;
+        final long SECOND_END = SECOND_START + HOUR_IN_MILLIS;
+        final long THIRD_START = TEST_START + (2 * WEEK_IN_MILLIS);
+        final long THIRD_END = THIRD_START + (2 * HOUR_IN_MILLIS);
+
+        stats.recordData(FIRST_START, FIRST_END,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+        stats.recordData(SECOND_START, SECOND_END,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+        stats.recordData(THIRD_START, THIRD_END,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+
+        // should have buckets: 2+1+2
+        assertEquals(5, stats.size());
+
+        assertIndexBeforeAfter(stats, 0, 0, Long.MIN_VALUE);
+        assertIndexBeforeAfter(stats, 0, 1, FIRST_START);
+        assertIndexBeforeAfter(stats, 0, 1, FIRST_START + MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 0, 2, FIRST_START + HOUR_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 1, 2, FIRST_START + HOUR_IN_MILLIS + MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 1, 2, FIRST_END - MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 1, 2, FIRST_END);
+        assertIndexBeforeAfter(stats, 1, 2, FIRST_END + MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 1, 2, SECOND_START - MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 1, 3, SECOND_START);
+        assertIndexBeforeAfter(stats, 2, 3, SECOND_END);
+        assertIndexBeforeAfter(stats, 2, 3, SECOND_END + MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 2, 3, THIRD_START - MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 2, 4, THIRD_START);
+        assertIndexBeforeAfter(stats, 3, 4, THIRD_START + MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 3, 4, THIRD_START + HOUR_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 4, 4, THIRD_END);
+        assertIndexBeforeAfter(stats, 4, 4, THIRD_END + MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 4, 4, Long.MAX_VALUE);
+    }
+
+    private static void assertIndexBeforeAfter(
+            NetworkStatsHistory stats, int before, int after, long time) {
+        assertEquals("unexpected before", before, stats.getIndexBefore(time));
+        assertEquals("unexpected after", after, stats.getIndexAfter(time));
+    }
+
+    private static long performVarLong(long before) throws Exception {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        writeVarLong(new DataOutputStream(out), before);
+
+        final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+        return readVarLong(new DataInputStream(in));
+    }
+
+    private static void assertConsistent(NetworkStatsHistory stats) {
+        // verify timestamps are monotonic
+        long lastStart = Long.MIN_VALUE;
+        NetworkStatsHistory.Entry entry = null;
+        for (int i = 0; i < stats.size(); i++) {
+            entry = stats.getValues(i, entry);
+            assertTrue(lastStart < entry.bucketStart);
+            lastStart = entry.bucketStart;
+        }
+    }
+
+    private static void assertValues(
+            NetworkStatsHistory stats, int index, long rxBytes, long txBytes) {
+        final NetworkStatsHistory.Entry entry = stats.getValues(index, null);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+    }
+
+    private static void assertValues(
+            NetworkStatsHistory stats, long start, long end, long rxBytes, long txBytes) {
+        final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+    }
+
+    private static void assertValues(NetworkStatsHistory stats, int index, long activeTime,
+            long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) {
+        final NetworkStatsHistory.Entry entry = stats.getValues(index, null);
+        assertEquals("unexpected activeTime", activeTime, entry.activeTime);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+        assertEquals("unexpected txPackets", txPackets, entry.txPackets);
+        assertEquals("unexpected operations", operations, entry.operations);
+    }
+
+    private static void assertFullValues(NetworkStatsHistory stats, long activeTime, long rxBytes,
+            long rxPackets, long txBytes, long txPackets, long operations) {
+        assertValues(stats, Long.MIN_VALUE, Long.MAX_VALUE, activeTime, rxBytes, rxPackets, txBytes,
+                txPackets, operations);
+    }
+
+    private static void assertValues(NetworkStatsHistory stats, long start, long end,
+            long activeTime, long rxBytes, long rxPackets, long txBytes, long txPackets,
+            long operations) {
+        final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null);
+        assertEquals("unexpected activeTime", activeTime, entry.activeTime);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+        assertEquals("unexpected txPackets", txPackets, entry.txPackets);
+        assertEquals("unexpected operations", operations, entry.operations);
+    }
+}
diff --git a/core/tests/coretests/src/android/net/NetworkStatsTest.java b/core/tests/coretests/src/android/net/NetworkStatsTest.java
new file mode 100644
index 0000000..6331964
--- /dev/null
+++ b/core/tests/coretests/src/android/net/NetworkStatsTest.java
@@ -0,0 +1,337 @@
+/*
+ * 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.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.SET_FOREGROUND;
+import static android.net.NetworkStats.SET_ALL;
+import static android.net.NetworkStats.IFACE_ALL;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.google.android.collect.Sets;
+
+import junit.framework.TestCase;
+
+import java.util.HashSet;
+
+@SmallTest
+public class NetworkStatsTest extends TestCase {
+
+    private static final String TEST_IFACE = "test0";
+    private static final String TEST_IFACE2 = "test2";
+    private static final int TEST_UID = 1001;
+    private static final long TEST_START = 1194220800000L;
+
+    public void testFindIndex() throws Exception {
+        final NetworkStats stats = new NetworkStats(TEST_START, 3)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 10)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 11)
+                .addValues(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 12);
+
+        assertEquals(2, stats.findIndex(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE));
+        assertEquals(2, stats.findIndex(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE));
+        assertEquals(0, stats.findIndex(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE));
+        assertEquals(-1, stats.findIndex(TEST_IFACE, 6, SET_DEFAULT, TAG_NONE));
+    }
+
+    public void testFindIndexHinted() {
+        final NetworkStats stats = new NetworkStats(TEST_START, 3)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 10)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 11)
+                .addValues(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 12)
+                .addValues(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 1024L, 8L, 0L, 0L, 10)
+                .addValues(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 1024L, 8L, 11)
+                .addValues(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 12);
+
+        // verify that we correctly find across regardless of hinting
+        for (int hint = 0; hint < stats.size(); hint++) {
+            assertEquals(0, stats.findIndexHinted(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, hint));
+            assertEquals(1, stats.findIndexHinted(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, hint));
+            assertEquals(2, stats.findIndexHinted(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, hint));
+            assertEquals(3, stats.findIndexHinted(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, hint));
+            assertEquals(4, stats.findIndexHinted(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, hint));
+            assertEquals(5, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, hint));
+            assertEquals(-1, stats.findIndexHinted(TEST_IFACE, 6, SET_DEFAULT, TAG_NONE, hint));
+        }
+    }
+
+    public void testAddEntryGrow() throws Exception {
+        final NetworkStats stats = new NetworkStats(TEST_START, 2);
+
+        assertEquals(0, stats.size());
+        assertEquals(2, stats.internalSize());
+
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, 1L, 1L, 2L, 2L, 3);
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, 2L, 2L, 2L, 2L, 4);
+
+        assertEquals(2, stats.size());
+        assertEquals(2, stats.internalSize());
+
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, 3L, 30L, 4L, 40L, 7);
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, 4L, 40L, 4L, 40L, 8);
+        stats.addValues(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, 5L, 50L, 5L, 50L, 10);
+
+        assertEquals(5, stats.size());
+        assertTrue(stats.internalSize() >= 5);
+
+        assertValues(stats, 0, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, 1L, 1L, 2L, 2L, 3);
+        assertValues(stats, 1, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, 2L, 2L, 2L, 2L, 4);
+        assertValues(stats, 2, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, 3L, 30L, 4L, 40L, 7);
+        assertValues(stats, 3, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, 4L, 40L, 4L, 40L, 8);
+        assertValues(stats, 4, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, 5L, 50L, 5L, 50L, 10);
+    }
+
+    public void testCombineExisting() throws Exception {
+        final NetworkStats stats = new NetworkStats(TEST_START, 10);
+
+        stats.addValues(TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 10);
+        stats.addValues(TEST_IFACE, 1001, SET_DEFAULT, 0xff, 128L, 1L, 128L, 1L, 2);
+        stats.combineValues(TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, -128L, -1L, -128L, -1L, -1);
+
+        assertValues(stats, 0, TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, 384L, 3L, 128L, 1L, 9);
+        assertValues(stats, 1, TEST_IFACE, 1001, SET_DEFAULT, 0xff, 128L, 1L, 128L, 1L, 2);
+
+        // now try combining that should create row
+        stats.combineValues(TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 3);
+        assertValues(stats, 2, TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 3);
+        stats.combineValues(TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 3);
+        assertValues(stats, 2, TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 256L, 2L, 256L, 2L, 6);
+    }
+
+    public void testSubtractIdenticalData() throws Exception {
+        final NetworkStats before = new NetworkStats(TEST_START, 2)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12);
+
+        final NetworkStats after = new NetworkStats(TEST_START, 2)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12);
+
+        final NetworkStats result = after.subtract(before);
+
+        // identical data should result in zero delta
+        assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0);
+        assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0);
+    }
+
+    public void testSubtractIdenticalRows() throws Exception {
+        final NetworkStats before = new NetworkStats(TEST_START, 2)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12);
+
+        final NetworkStats after = new NetworkStats(TEST_START, 2)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1025L, 9L, 2L, 1L, 15)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 3L, 1L, 1028L, 9L, 20);
+
+        final NetworkStats result = after.subtract(before);
+
+        // expect delta between measurements
+        assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1L, 1L, 2L, 1L, 4);
+        assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 3L, 1L, 4L, 1L, 8);
+    }
+
+    public void testSubtractNewRows() throws Exception {
+        final NetworkStats before = new NetworkStats(TEST_START, 2)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12);
+
+        final NetworkStats after = new NetworkStats(TEST_START, 3)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12)
+                .addValues(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 20);
+
+        final NetworkStats result = after.subtract(before);
+
+        // its okay to have new rows
+        assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0);
+        assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0);
+        assertValues(result, 2, TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 20);
+    }
+
+    public void testSubtractMissingRows() throws Exception {
+        final NetworkStats before = new NetworkStats(TEST_START, 2)
+                .addValues(TEST_IFACE, UID_ALL, SET_DEFAULT, TAG_NONE, 1024L, 0L, 0L, 0L, 0)
+                .addValues(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 2048L, 0L, 0L, 0L, 0);
+
+        final NetworkStats after = new NetworkStats(TEST_START, 1)
+                .addValues(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 2049L, 2L, 3L, 4L, 0);
+
+        final NetworkStats result = after.subtract(before);
+
+        // should silently drop omitted rows
+        assertEquals(1, result.size());
+        assertValues(result, 0, TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 1L, 2L, 3L, 4L, 0);
+        assertEquals(4L, result.getTotalBytes());
+    }
+
+    public void testTotalBytes() throws Exception {
+        final NetworkStats iface = new NetworkStats(TEST_START, 2)
+                .addValues(TEST_IFACE, UID_ALL, SET_DEFAULT, TAG_NONE, 128L, 0L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 256L, 0L, 0L, 0L, 0L);
+        assertEquals(384L, iface.getTotalBytes());
+
+        final NetworkStats uidSet = new NetworkStats(TEST_START, 3)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 32L, 0L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 32L, 0L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_FOREGROUND, TAG_NONE, 32L, 0L, 0L, 0L, 0L);
+        assertEquals(96L, uidSet.getTotalBytes());
+
+        final NetworkStats uidTag = new NetworkStats(TEST_START, 3)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 16L, 0L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 16L, 0L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 8L, 0L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 16L, 0L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 16L, 0L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 8L, 0L, 0L, 0L, 0L);
+        assertEquals(64L, uidTag.getTotalBytes());
+    }
+
+    public void testGroupedByIfaceEmpty() throws Exception {
+        final NetworkStats uidStats = new NetworkStats(TEST_START, 3);
+        final NetworkStats grouped = uidStats.groupedByIface();
+
+        assertEquals(0, uidStats.size());
+        assertEquals(0, grouped.size());
+    }
+
+    public void testGroupedByIfaceAll() throws Exception {
+        final NetworkStats uidStats = new NetworkStats(TEST_START, 3)
+                .addValues(IFACE_ALL, 100, SET_ALL, TAG_NONE, 128L, 8L, 0L, 2L, 20L)
+                .addValues(IFACE_ALL, 101, SET_FOREGROUND, TAG_NONE, 128L, 8L, 0L, 2L, 20L);
+        final NetworkStats grouped = uidStats.groupedByIface();
+
+        assertEquals(2, uidStats.size());
+        assertEquals(1, grouped.size());
+
+        assertValues(grouped, 0, IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, 256L, 16L, 0L, 4L, 0L);
+    }
+
+    public void testGroupedByIface() throws Exception {
+        final NetworkStats uidStats = new NetworkStats(TEST_START, 3)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 2L, 20L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 512L, 32L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 64L, 4L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 512L, 32L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 128L, 8L, 0L, 0L, 0L);
+
+        final NetworkStats grouped = uidStats.groupedByIface();
+
+        assertEquals(6, uidStats.size());
+
+        assertEquals(2, grouped.size());
+        assertValues(grouped, 0, TEST_IFACE, UID_ALL, SET_ALL, TAG_NONE, 256L, 16L, 0L, 2L, 0L);
+        assertValues(grouped, 1, TEST_IFACE2, UID_ALL, SET_ALL, TAG_NONE, 1024L, 64L, 0L, 0L, 0L);
+    }
+
+    public void testAddAllValues() {
+        final NetworkStats first = new NetworkStats(TEST_START, 5)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 32L, 0L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, 32L, 0L, 0L, 0L, 0L);
+
+        final NetworkStats second = new NetworkStats(TEST_START, 2)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 32L, 0L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 32L, 0L, 0L, 0L, 0L);
+
+        first.combineAllValues(second);
+
+        assertEquals(3, first.size());
+        assertValues(first, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 64L, 0L, 0L, 0L, 0L);
+        assertValues(first, 1, TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, 32L, 0L, 0L, 0L, 0L);
+        assertValues(first, 2, TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 32L, 0L, 0L, 0L, 0L);
+    }
+
+    public void testGetTotal() {
+        final NetworkStats stats = new NetworkStats(TEST_START, 3)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 2L, 20L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 512L, 32L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 64L, 4L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 512L, 32L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 128L, 8L, 0L, 0L, 0L);
+
+        assertValues(stats.getTotal(null), 1280L, 80L, 0L, 2L, 20L);
+        assertValues(stats.getTotal(null, 100), 1152L, 72L, 0L, 2L, 20L);
+        assertValues(stats.getTotal(null, 101), 128L, 8L, 0L, 0L, 0L);
+
+        final HashSet<String> ifaces = Sets.newHashSet();
+        assertValues(stats.getTotal(null, ifaces), 0L, 0L, 0L, 0L, 0L);
+
+        ifaces.add(TEST_IFACE2);
+        assertValues(stats.getTotal(null, ifaces), 1024L, 64L, 0L, 0L, 0L);
+    }
+
+    public void testWithoutUid() throws Exception {
+        final NetworkStats before = new NetworkStats(TEST_START, 3)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 2L, 20L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 512L, 32L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 64L, 4L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 512L, 32L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 0L, 0L)
+                .addValues(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 128L, 8L, 0L, 0L, 0L);
+
+        final NetworkStats after = before.withoutUids(new int[] { 100 });
+        assertEquals(6, before.size());
+        assertEquals(2, after.size());
+        assertValues(after, 0, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 0L, 0L);
+        assertValues(after, 1, TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 128L, 8L, 0L, 0L, 0L);
+    }
+
+    public void testClone() throws Exception {
+        final NetworkStats original = new NetworkStats(TEST_START, 5)
+                .addValues(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 2L, 20L)
+                .addValues(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 512L, 32L, 0L, 0L, 0L);
+
+        // make clone and mutate original
+        final NetworkStats clone = original.clone();
+        original.addValues(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 0L, 0L);
+
+        assertEquals(3, original.size());
+        assertEquals(2, clone.size());
+
+        assertEquals(128L + 512L + 128L, original.getTotalBytes());
+        assertEquals(128L + 512L, clone.getTotalBytes());
+    }
+
+    private static void assertValues(NetworkStats stats, int index, String iface, int uid, int set,
+            int tag, long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) {
+        final NetworkStats.Entry entry = stats.getValues(index, null);
+        assertValues(entry, iface, uid, set, tag);
+        assertValues(entry, rxBytes, rxPackets, txBytes, txPackets, operations);
+    }
+
+    private static void assertValues(
+            NetworkStats.Entry entry, String iface, int uid, int set, int tag) {
+        assertEquals(iface, entry.iface);
+        assertEquals(uid, entry.uid);
+        assertEquals(set, entry.set);
+        assertEquals(tag, entry.tag);
+    }
+
+    private static void assertValues(NetworkStats.Entry entry, long rxBytes, long rxPackets,
+            long txBytes, long txPackets, long operations) {
+        assertEquals(rxBytes, entry.rxBytes);
+        assertEquals(rxPackets, entry.rxPackets);
+        assertEquals(txBytes, entry.txBytes);
+        assertEquals(txPackets, entry.txPackets);
+        assertEquals(operations, entry.operations);
+    }
+
+}
diff --git a/core/tests/coretests/src/android/net/RouteInfoTest.java b/core/tests/coretests/src/android/net/RouteInfoTest.java
new file mode 100644
index 0000000..01283a6
--- /dev/null
+++ b/core/tests/coretests/src/android/net/RouteInfoTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+import java.lang.reflect.Method;
+import java.net.InetAddress;
+
+import android.net.LinkAddress;
+import android.net.RouteInfo;
+import android.os.Parcel;
+
+import junit.framework.TestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+public class RouteInfoTest extends TestCase {
+
+    private InetAddress Address(String addr) {
+        return InetAddress.parseNumericAddress(addr);
+    }
+
+    private LinkAddress Prefix(String prefix) {
+        String[] parts = prefix.split("/");
+        return new LinkAddress(Address(parts[0]), Integer.parseInt(parts[1]));
+    }
+
+    @SmallTest
+    public void testConstructor() {
+        RouteInfo r;
+
+        // Invalid input.
+        try {
+            r = new RouteInfo((LinkAddress) null, null, "rmnet0");
+            fail("Expected RuntimeException:  destination and gateway null");
+        } catch(RuntimeException e) {}
+
+        // Null destination is default route.
+        r = new RouteInfo((LinkAddress) null, Address("2001:db8::1"), null);
+        assertEquals(Prefix("::/0"), r.getDestination());
+        assertEquals(Address("2001:db8::1"), r.getGateway());
+        assertNull(r.getInterface());
+
+        r = new RouteInfo((LinkAddress) null, Address("192.0.2.1"), "wlan0");
+        assertEquals(Prefix("0.0.0.0/0"), r.getDestination());
+        assertEquals(Address("192.0.2.1"), r.getGateway());
+        assertEquals("wlan0", r.getInterface());
+
+        // Null gateway sets gateway to unspecified address (why?).
+        r = new RouteInfo(Prefix("2001:db8:beef:cafe::/48"), null, "lo");
+        assertEquals(Prefix("2001:db8:beef::/48"), r.getDestination());
+        assertEquals(Address("::"), r.getGateway());
+        assertEquals("lo", r.getInterface());
+
+        r = new RouteInfo(Prefix("192.0.2.5/24"), null);
+        assertEquals(Prefix("192.0.2.0/24"), r.getDestination());
+        assertEquals(Address("0.0.0.0"), r.getGateway());
+        assertNull(r.getInterface());
+    }
+
+    public void testMatches() {
+        class PatchedRouteInfo extends RouteInfo {
+            public PatchedRouteInfo(LinkAddress destination, InetAddress gateway, String iface) {
+                super(destination, gateway, iface);
+            }
+
+            public boolean matches(InetAddress destination) {
+                return super.matches(destination);
+            }
+        }
+
+        RouteInfo r;
+
+        r = new PatchedRouteInfo(Prefix("2001:db8:f00::ace:d00d/127"), null, "rmnet0");
+        assertTrue(r.matches(Address("2001:db8:f00::ace:d00c")));
+        assertTrue(r.matches(Address("2001:db8:f00::ace:d00d")));
+        assertFalse(r.matches(Address("2001:db8:f00::ace:d00e")));
+        assertFalse(r.matches(Address("2001:db8:f00::bad:d00d")));
+        assertFalse(r.matches(Address("2001:4868:4860::8888")));
+
+        r = new PatchedRouteInfo(Prefix("192.0.2.0/23"), null, "wlan0");
+        assertTrue(r.matches(Address("192.0.2.43")));
+        assertTrue(r.matches(Address("192.0.3.21")));
+        assertFalse(r.matches(Address("192.0.0.21")));
+        assertFalse(r.matches(Address("8.8.8.8")));
+
+        RouteInfo ipv6Default = new PatchedRouteInfo(Prefix("::/0"), null, "rmnet0");
+        assertTrue(ipv6Default.matches(Address("2001:db8::f00")));
+        assertFalse(ipv6Default.matches(Address("192.0.2.1")));
+
+        RouteInfo ipv4Default = new PatchedRouteInfo(Prefix("0.0.0.0/0"), null, "rmnet0");
+        assertTrue(ipv4Default.matches(Address("255.255.255.255")));
+        assertTrue(ipv4Default.matches(Address("192.0.2.1")));
+        assertFalse(ipv4Default.matches(Address("2001:db8::f00")));
+    }
+
+    private void assertAreEqual(Object o1, Object o2) {
+        assertTrue(o1.equals(o2));
+        assertTrue(o2.equals(o1));
+    }
+
+    private void assertAreNotEqual(Object o1, Object o2) {
+        assertFalse(o1.equals(o2));
+        assertFalse(o2.equals(o1));
+    }
+
+    public void testEquals() {
+        // IPv4
+        RouteInfo r1 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::1"), "wlan0");
+        RouteInfo r2 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::1"), "wlan0");
+        assertAreEqual(r1, r2);
+
+        RouteInfo r3 = new RouteInfo(Prefix("2001:db8:ace::/49"), Address("2001:db8::1"), "wlan0");
+        RouteInfo r4 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::2"), "wlan0");
+        RouteInfo r5 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::1"), "rmnet0");
+        assertAreNotEqual(r1, r3);
+        assertAreNotEqual(r1, r4);
+        assertAreNotEqual(r1, r5);
+
+        // IPv6
+        r1 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), "wlan0");
+        r2 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), "wlan0");
+        assertAreEqual(r1, r2);
+
+        r3 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0");
+        r4 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.2"), "wlan0");
+        r5 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), "rmnet0");
+        assertAreNotEqual(r1, r3);
+        assertAreNotEqual(r1, r4);
+        assertAreNotEqual(r1, r5);
+
+        // Interfaces (but not destinations or gateways) can be null.
+        r1 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), null);
+        r2 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), null);
+        r3 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0");
+        assertAreEqual(r1, r2);
+        assertAreNotEqual(r1, r3);
+    }
+
+    public void testHostRoute() {
+      RouteInfo r;
+
+      r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0");
+      assertFalse(r.isHostRoute());
+
+      r = new RouteInfo(Prefix("::/0"), Address("::"), "wlan0");
+      assertFalse(r.isHostRoute());
+
+      r = new RouteInfo(Prefix("192.0.2.0/24"), null, "wlan0");
+      assertFalse(r.isHostRoute());
+
+      r = new RouteInfo(Prefix("2001:db8::/48"), null, "wlan0");
+      assertFalse(r.isHostRoute());
+
+      r = new RouteInfo(Prefix("192.0.2.0/32"), Address("0.0.0.0"), "wlan0");
+      assertTrue(r.isHostRoute());
+
+      r = new RouteInfo(Prefix("2001:db8::/128"), Address("::"), "wlan0");
+      assertTrue(r.isHostRoute());
+
+      r = new RouteInfo(Prefix("192.0.2.0/32"), null, "wlan0");
+      assertTrue(r.isHostRoute());
+
+      r = new RouteInfo(Prefix("2001:db8::/128"), null, "wlan0");
+      assertTrue(r.isHostRoute());
+
+      r = new RouteInfo(Prefix("::/128"), Address("fe80::"), "wlan0");
+      assertTrue(r.isHostRoute());
+
+      r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0");
+      assertTrue(r.isHostRoute());
+    }
+
+    public RouteInfo passThroughParcel(RouteInfo r) {
+        Parcel p = Parcel.obtain();
+        RouteInfo r2 = null;
+        try {
+            r.writeToParcel(p, 0);
+            p.setDataPosition(0);
+            r2 = RouteInfo.CREATOR.createFromParcel(p);
+        } finally {
+            p.recycle();
+        }
+        assertNotNull(r2);
+        return r2;
+    }
+
+    public void assertParcelingIsLossless(RouteInfo r) {
+      RouteInfo r2 = passThroughParcel(r);
+      assertEquals(r, r2);
+    }
+
+    public void testParceling() {
+        RouteInfo r;
+
+        r = new RouteInfo(Prefix("::/0"), Address("2001:db8::"), null);
+        assertParcelingIsLossless(r);
+
+        r = new RouteInfo(Prefix("192.0.2.0/24"), null, "wlan0");
+        assertParcelingIsLossless(r);
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/net/NetworkStatsFactoryTest.java b/core/tests/coretests/src/com/android/internal/net/NetworkStatsFactoryTest.java
new file mode 100644
index 0000000..d3dd01a
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/net/NetworkStatsFactoryTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.internal.net;
+
+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.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static com.android.server.NetworkManagementSocketTagger.kernelToTag;
+
+import android.content.res.Resources;
+import android.net.NetworkStats;
+import android.net.TrafficStats;
+import android.test.AndroidTestCase;
+
+import com.android.frameworks.coretests.R;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+/**
+ * Tests for {@link NetworkStatsFactory}.
+ */
+public class NetworkStatsFactoryTest extends AndroidTestCase {
+    private File mTestProc;
+    private NetworkStatsFactory mFactory;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mTestProc = new File(getContext().getFilesDir(), "proc");
+        if (mTestProc.exists()) {
+            IoUtils.deleteContents(mTestProc);
+        }
+
+        mFactory = new NetworkStatsFactory(mTestProc);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mFactory = null;
+
+        if (mTestProc.exists()) {
+            IoUtils.deleteContents(mTestProc);
+        }
+
+        super.tearDown();
+    }
+
+    public void testNetworkStatsDetail() throws Exception {
+        stageFile(R.raw.xt_qtaguid_typical, new File(mTestProc, "net/xt_qtaguid/stats"));
+
+        final NetworkStats stats = mFactory.readNetworkStatsDetail();
+        assertEquals(70, stats.size());
+        assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, 18621L, 2898L);
+        assertStatsEntry(stats, "wlan0", 10011, SET_DEFAULT, 0x0, 35777L, 5718L);
+        assertStatsEntry(stats, "wlan0", 10021, SET_DEFAULT, 0x7fffff01, 562386L, 49228L);
+        assertStatsEntry(stats, "rmnet1", 10021, SET_DEFAULT, 0x30100000, 219110L, 227423L);
+        assertStatsEntry(stats, "rmnet2", 10001, SET_DEFAULT, 0x0, 1125899906842624L, 984L);
+    }
+
+    public void testKernelTags() throws Exception {
+        assertEquals(0, kernelToTag("0x0000000000000000"));
+        assertEquals(0x32, kernelToTag("0x0000003200000000"));
+        assertEquals(2147483647, kernelToTag("0x7fffffff00000000"));
+        assertEquals(0, kernelToTag("0x0000000000000000"));
+        assertEquals(2147483136, kernelToTag("0x7FFFFE0000000000"));
+
+        assertEquals(0, kernelToTag("0x0"));
+        assertEquals(0, kernelToTag("0xf00d"));
+        assertEquals(1, kernelToTag("0x100000000"));
+        assertEquals(14438007, kernelToTag("0xdc4e7700000000"));
+        assertEquals(TrafficStats.TAG_SYSTEM_DOWNLOAD, kernelToTag("0xffffff0100000000"));
+    }
+
+    public void testNetworkStatsWithSet() throws Exception {
+        stageFile(R.raw.xt_qtaguid_typical, new File(mTestProc, "net/xt_qtaguid/stats"));
+
+        final NetworkStats stats = mFactory.readNetworkStatsDetail();
+        assertEquals(70, stats.size());
+        assertStatsEntry(stats, "rmnet1", 10021, SET_DEFAULT, 0x30100000, 219110L, 578L, 227423L, 676L);
+        assertStatsEntry(stats, "rmnet1", 10021, SET_FOREGROUND, 0x30100000, 742L, 3L, 1265L, 3L);
+    }
+
+    public void testNetworkStatsSingle() throws Exception {
+        stageFile(R.raw.xt_qtaguid_iface_typical, new File(mTestProc, "net/xt_qtaguid/iface_stat_all"));
+
+        final NetworkStats stats = mFactory.readNetworkStatsSummaryDev();
+        assertEquals(6, stats.size());
+        assertStatsEntry(stats, "rmnet0", UID_ALL, SET_ALL, TAG_NONE, 2112L, 24L, 700L, 10L);
+        assertStatsEntry(stats, "test1", UID_ALL, SET_ALL, TAG_NONE, 6L, 8L, 10L, 12L);
+        assertStatsEntry(stats, "test2", UID_ALL, SET_ALL, TAG_NONE, 1L, 2L, 3L, 4L);
+    }
+
+    public void testNetworkStatsXt() throws Exception {
+        stageFile(R.raw.xt_qtaguid_iface_fmt_typical,
+                new File(mTestProc, "net/xt_qtaguid/iface_stat_fmt"));
+
+        final NetworkStats stats = mFactory.readNetworkStatsSummaryXt();
+        assertEquals(3, stats.size());
+        assertStatsEntry(stats, "rmnet0", UID_ALL, SET_ALL, TAG_NONE, 6824L, 16L, 5692L, 10L);
+        assertStatsEntry(stats, "rmnet1", UID_ALL, SET_ALL, TAG_NONE, 11153922L, 8051L, 190226L, 2468L);
+        assertStatsEntry(stats, "rmnet2", UID_ALL, SET_ALL, TAG_NONE, 4968L, 35L, 3081L, 39L);
+    }
+
+    /**
+     * Copy a {@link Resources#openRawResource(int)} into {@link File} for
+     * testing purposes.
+     */
+    private void stageFile(int rawId, File file) throws Exception {
+        new File(file.getParent()).mkdirs();
+        InputStream in = null;
+        OutputStream out = null;
+        try {
+            in = getContext().getResources().openRawResource(rawId);
+            out = new FileOutputStream(file);
+            Streams.copy(in, out);
+        } finally {
+            IoUtils.closeQuietly(in);
+            IoUtils.closeQuietly(out);
+        }
+    }
+
+    private void stageLong(long value, File file) throws Exception {
+        new File(file.getParent()).mkdirs();
+        FileWriter out = null;
+        try {
+            out = new FileWriter(file);
+            out.write(Long.toString(value));
+        } finally {
+            IoUtils.closeQuietly(out);
+        }
+    }
+
+    private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set,
+            int tag, long rxBytes, long txBytes) {
+        final int i = stats.findIndex(iface, uid, set, tag);
+        final NetworkStats.Entry entry = stats.getValues(i, null);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+    }
+
+    private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set,
+            int tag, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+        final int i = stats.findIndex(iface, uid, set, tag);
+        final NetworkStats.Entry entry = stats.getValues(i, null);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+        assertEquals("unexpected txPackets", txPackets, entry.txPackets);
+    }
+
+}
diff --git a/framework/Android.bp b/framework/Android.bp
new file mode 100644
index 0000000..93ef3bf
--- /dev/null
+++ b/framework/Android.bp
@@ -0,0 +1,185 @@
+//
+// 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 {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "framework-connectivity-protos",
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    proto: {
+        type: "nano",
+    },
+    srcs: [
+        // TODO: consider moving relevant .proto files directly to the module directory
+        ":framework-javastream-protos",
+    ],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+    ],
+    jarjar_rules: "jarjar-rules-proto.txt",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+filegroup {
+    name: "framework-connectivity-internal-sources",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.aidl",
+    ],
+    path: "src",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+filegroup {
+    name: "framework-connectivity-aidl-export-sources",
+    srcs: [
+        "aidl-export/**/*.aidl",
+    ],
+    path: "aidl-export",
+    visibility: [
+        "//visibility:private",
+    ],
+}
+
+// TODO: use a java_library in the bootclasspath instead
+filegroup {
+    name: "framework-connectivity-sources",
+    srcs: [
+        ":framework-connectivity-internal-sources",
+        ":framework-connectivity-aidl-export-sources",
+    ],
+    visibility: [
+        "//frameworks/base",
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+}
+
+java_sdk_library {
+    name: "framework-connectivity",
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    defaults: ["framework-module-defaults"],
+    installable: true,
+    srcs: [
+        ":framework-connectivity-sources",
+        ":net-utils-framework-common-srcs",
+    ],
+    aidl: {
+        include_dirs: [
+            // Include directories for parcelables that are part of the stable API, and need a
+            // one-line "parcelable X" .aidl declaration to be used in AIDL interfaces.
+            // TODO(b/180293679): remove these dependencies as they should not be necessary once
+            // the module builds against API (the parcelable declarations exist in framework.aidl)
+            "frameworks/base/core/java", // For framework parcelables
+            "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+        ],
+    },
+    impl_only_libs: [
+        // TODO (b/183097033) remove once module_current includes core_platform
+        "stable.core.platform.api.stubs",
+        "framework-tethering.stubs.module_lib",
+        "framework-wifi.stubs.module_lib",
+        "net-utils-device-common",
+    ],
+    libs: [
+        "unsupportedappusage",
+    ],
+    static_libs: [
+        "framework-connectivity-protos",
+    ],
+    jarjar_rules: "jarjar-rules.txt",
+    permitted_packages: ["android.net"],
+    impl_library_visibility: [
+        "//packages/modules/Connectivity/Tethering/apex",
+        // In preparation for future move
+        "//packages/modules/Connectivity/apex",
+        "//packages/modules/Connectivity/service",
+        "//frameworks/base/packages/Connectivity/service",
+        "//frameworks/base",
+
+        // Tests using hidden APIs
+        "//external/sl4a:__subpackages__",
+        "//frameworks/base/tests/net:__subpackages__",
+        "//frameworks/libs/net/common/testutils",
+        "//frameworks/libs/net/common/tests:__subpackages__",
+        "//frameworks/opt/telephony/tests/telephonytests",
+        "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
+        "//packages/modules/Connectivity/tests:__subpackages__",
+        "//packages/modules/NetworkStack/tests:__subpackages__",
+    ],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+    ],
+}
+
+cc_defaults {
+    name: "libframework-connectivity-defaults",
+    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",
+    ],
+    shared_libs: [
+        "liblog",
+        "libnativehelper",
+    ],
+    header_libs: [
+        "dnsproxyd_protocol_headers",
+    ],
+}
+
+cc_library_static {
+    name: "libconnectivityframeworkutils",
+    defaults: ["libframework-connectivity-defaults"],
+    srcs: [
+        "jni/android_net_NetworkUtils.cpp",
+    ],
+    shared_libs: ["libandroid_net"],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.tethering",
+    ],
+}
+
+cc_library_shared {
+    name: "libframework-connectivity-jni",
+    min_sdk_version: "30",
+    defaults: ["libframework-connectivity-defaults"],
+    srcs: [
+        "jni/android_net_NetworkUtils.cpp",
+        "jni/onload.cpp",
+    ],
+    shared_libs: ["libandroid"],
+    stl: "libc++_static",
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
diff --git a/framework/aidl-export/android/net/CaptivePortalData.aidl b/framework/aidl-export/android/net/CaptivePortalData.aidl
new file mode 100644
index 0000000..1d57ee7
--- /dev/null
+++ b/framework/aidl-export/android/net/CaptivePortalData.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+@JavaOnlyStableParcelable parcelable CaptivePortalData;
diff --git a/framework/aidl-export/android/net/ConnectivityDiagnosticsManager.aidl b/framework/aidl-export/android/net/ConnectivityDiagnosticsManager.aidl
new file mode 100644
index 0000000..82ba0ca
--- /dev/null
+++ b/framework/aidl-export/android/net/ConnectivityDiagnosticsManager.aidl
@@ -0,0 +1,21 @@
+/**
+ *
+ * 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;
+
+parcelable ConnectivityDiagnosticsManager.ConnectivityReport;
+parcelable ConnectivityDiagnosticsManager.DataStallReport;
\ No newline at end of file
diff --git a/framework/aidl-export/android/net/DhcpInfo.aidl b/framework/aidl-export/android/net/DhcpInfo.aidl
new file mode 100644
index 0000000..29cd21f
--- /dev/null
+++ b/framework/aidl-export/android/net/DhcpInfo.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2008, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); 
+ * you may not use this file except in compliance with the License. 
+ * You may obtain a copy of the License at 
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0 
+ *
+ * Unless required by applicable law or agreed to in writing, software 
+ * distributed under the License is distributed on an "AS IS" BASIS, 
+ * WITHOUT 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 DhcpInfo;
diff --git a/framework/aidl-export/android/net/IpConfiguration.aidl b/framework/aidl-export/android/net/IpConfiguration.aidl
new file mode 100644
index 0000000..7a30f0e
--- /dev/null
+++ b/framework/aidl-export/android/net/IpConfiguration.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable IpConfiguration;
diff --git a/framework/aidl-export/android/net/IpPrefix.aidl b/framework/aidl-export/android/net/IpPrefix.aidl
new file mode 100644
index 0000000..3495efc
--- /dev/null
+++ b/framework/aidl-export/android/net/IpPrefix.aidl
@@ -0,0 +1,22 @@
+/**
+ *
+ * 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;
+
+// @JavaOnlyStableParcelable only affects the parcelable when built as stable aidl (aidl_interface
+// build rule).
+@JavaOnlyStableParcelable parcelable IpPrefix;
diff --git a/framework/aidl-export/android/net/KeepalivePacketData.aidl b/framework/aidl-export/android/net/KeepalivePacketData.aidl
new file mode 100644
index 0000000..d456b53
--- /dev/null
+++ b/framework/aidl-export/android/net/KeepalivePacketData.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable KeepalivePacketData;
diff --git a/framework/aidl-export/android/net/LinkAddress.aidl b/framework/aidl-export/android/net/LinkAddress.aidl
new file mode 100644
index 0000000..9c804db
--- /dev/null
+++ b/framework/aidl-export/android/net/LinkAddress.aidl
@@ -0,0 +1,21 @@
+/**
+ *
+ * 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.
+ */
+
+package android.net;
+
+@JavaOnlyStableParcelable parcelable LinkAddress;
+
diff --git a/framework/aidl-export/android/net/LinkProperties.aidl b/framework/aidl-export/android/net/LinkProperties.aidl
new file mode 100644
index 0000000..a8b3c7b
--- /dev/null
+++ b/framework/aidl-export/android/net/LinkProperties.aidl
@@ -0,0 +1,20 @@
+/*
+**
+** 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.
+*/
+
+package android.net;
+
+@JavaOnlyStableParcelable parcelable LinkProperties;
diff --git a/framework/aidl-export/android/net/MacAddress.aidl b/framework/aidl-export/android/net/MacAddress.aidl
new file mode 100644
index 0000000..48a18a7
--- /dev/null
+++ b/framework/aidl-export/android/net/MacAddress.aidl
@@ -0,0 +1,20 @@
+/**
+ *
+ * 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;
+
+@JavaOnlyStableParcelable parcelable MacAddress;
diff --git a/framework/aidl-export/android/net/Network.aidl b/framework/aidl-export/android/net/Network.aidl
new file mode 100644
index 0000000..0562202
--- /dev/null
+++ b/framework/aidl-export/android/net/Network.aidl
@@ -0,0 +1,20 @@
+/*
+**
+** 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;
+
+@JavaOnlyStableParcelable parcelable Network;
diff --git a/framework/aidl-export/android/net/NetworkAgentConfig.aidl b/framework/aidl-export/android/net/NetworkAgentConfig.aidl
new file mode 100644
index 0000000..cb70bdd
--- /dev/null
+++ b/framework/aidl-export/android/net/NetworkAgentConfig.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable NetworkAgentConfig;
diff --git a/framework/aidl-export/android/net/NetworkCapabilities.aidl b/framework/aidl-export/android/net/NetworkCapabilities.aidl
new file mode 100644
index 0000000..01d3286
--- /dev/null
+++ b/framework/aidl-export/android/net/NetworkCapabilities.aidl
@@ -0,0 +1,21 @@
+/*
+**
+** 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;
+
+@JavaOnlyStableParcelable parcelable NetworkCapabilities;
+
diff --git a/framework/aidl-export/android/net/NetworkInfo.aidl b/framework/aidl-export/android/net/NetworkInfo.aidl
new file mode 100644
index 0000000..f501873
--- /dev/null
+++ b/framework/aidl-export/android/net/NetworkInfo.aidl
@@ -0,0 +1,19 @@
+/**
+ * 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;
+
+parcelable NetworkInfo;
diff --git a/framework/aidl-export/android/net/NetworkRequest.aidl b/framework/aidl-export/android/net/NetworkRequest.aidl
new file mode 100644
index 0000000..508defc
--- /dev/null
+++ b/framework/aidl-export/android/net/NetworkRequest.aidl
@@ -0,0 +1,20 @@
+/**
+ * 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;
+
+parcelable NetworkRequest;
+
diff --git a/framework/aidl-export/android/net/NetworkScore.aidl b/framework/aidl-export/android/net/NetworkScore.aidl
new file mode 100644
index 0000000..af12dcf
--- /dev/null
+++ b/framework/aidl-export/android/net/NetworkScore.aidl
@@ -0,0 +1,20 @@
+/**
+ * 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 NetworkScore;
+
diff --git a/framework/aidl-export/android/net/OemNetworkPreferences.aidl b/framework/aidl-export/android/net/OemNetworkPreferences.aidl
new file mode 100644
index 0000000..2b6a4ce
--- /dev/null
+++ b/framework/aidl-export/android/net/OemNetworkPreferences.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable OemNetworkPreferences;
diff --git a/framework/aidl-export/android/net/ProxyInfo.aidl b/framework/aidl-export/android/net/ProxyInfo.aidl
new file mode 100644
index 0000000..a5d0c12
--- /dev/null
+++ b/framework/aidl-export/android/net/ProxyInfo.aidl
@@ -0,0 +1,21 @@
+/*
+**
+** 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.
+*/
+
+package android.net;
+
+@JavaOnlyStableParcelable parcelable ProxyInfo;
+
diff --git a/framework/aidl-export/android/net/QosFilterParcelable.aidl b/framework/aidl-export/android/net/QosFilterParcelable.aidl
new file mode 100644
index 0000000..312d635
--- /dev/null
+++ b/framework/aidl-export/android/net/QosFilterParcelable.aidl
@@ -0,0 +1,21 @@
+/*
+**
+** 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;
+
+parcelable QosFilterParcelable;
+
diff --git a/framework/aidl-export/android/net/QosSession.aidl b/framework/aidl-export/android/net/QosSession.aidl
new file mode 100644
index 0000000..c2cf366
--- /dev/null
+++ b/framework/aidl-export/android/net/QosSession.aidl
@@ -0,0 +1,21 @@
+/*
+**
+** 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;
+
+parcelable QosSession;
+
diff --git a/framework/aidl-export/android/net/QosSocketInfo.aidl b/framework/aidl-export/android/net/QosSocketInfo.aidl
new file mode 100644
index 0000000..476c090
--- /dev/null
+++ b/framework/aidl-export/android/net/QosSocketInfo.aidl
@@ -0,0 +1,21 @@
+/*
+**
+** 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;
+
+parcelable QosSocketInfo;
+
diff --git a/framework/aidl-export/android/net/RouteInfo.aidl b/framework/aidl-export/android/net/RouteInfo.aidl
new file mode 100644
index 0000000..7af9fda
--- /dev/null
+++ b/framework/aidl-export/android/net/RouteInfo.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;
+
+@JavaOnlyStableParcelable parcelable RouteInfo;
diff --git a/framework/aidl-export/android/net/StaticIpConfiguration.aidl b/framework/aidl-export/android/net/StaticIpConfiguration.aidl
new file mode 100644
index 0000000..8aac701
--- /dev/null
+++ b/framework/aidl-export/android/net/StaticIpConfiguration.aidl
@@ -0,0 +1,20 @@
+/*
+**
+** 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;
+
+@JavaOnlyStableParcelable parcelable StaticIpConfiguration;
\ No newline at end of file
diff --git a/framework/aidl-export/android/net/TestNetworkInterface.aidl b/framework/aidl-export/android/net/TestNetworkInterface.aidl
new file mode 100644
index 0000000..e1f4f9f
--- /dev/null
+++ b/framework/aidl-export/android/net/TestNetworkInterface.aidl
@@ -0,0 +1,20 @@
+/*
+ * 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;
+
+/** @hide */
+parcelable TestNetworkInterface;
diff --git a/framework/aidl-export/android/net/apf/ApfCapabilities.aidl b/framework/aidl-export/android/net/apf/ApfCapabilities.aidl
new file mode 100644
index 0000000..7c4d4c2
--- /dev/null
+++ b/framework/aidl-export/android/net/apf/ApfCapabilities.aidl
@@ -0,0 +1,20 @@
+/*
+**
+** 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.apf;
+
+@JavaOnlyStableParcelable parcelable ApfCapabilities;
\ No newline at end of file
diff --git a/framework/api/current.txt b/framework/api/current.txt
new file mode 100644
index 0000000..7692e30
--- /dev/null
+++ b/framework/api/current.txt
@@ -0,0 +1,474 @@
+// Signature format: 2.0
+package android.net {
+
+  public class CaptivePortal implements android.os.Parcelable {
+    method public int describeContents();
+    method public void ignoreNetwork();
+    method public void reportCaptivePortalDismissed();
+    method public void writeToParcel(android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.CaptivePortal> CREATOR;
+  }
+
+  public class ConnectivityDiagnosticsManager {
+    method public void registerConnectivityDiagnosticsCallback(@NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback);
+    method public void unregisterConnectivityDiagnosticsCallback(@NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback);
+  }
+
+  public abstract static class ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback {
+    ctor public ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback();
+    method public void onConnectivityReportAvailable(@NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityReport);
+    method public void onDataStallSuspected(@NonNull android.net.ConnectivityDiagnosticsManager.DataStallReport);
+    method public void onNetworkConnectivityReported(@NonNull android.net.Network, boolean);
+  }
+
+  public static final class ConnectivityDiagnosticsManager.ConnectivityReport implements android.os.Parcelable {
+    ctor public ConnectivityDiagnosticsManager.ConnectivityReport(@NonNull android.net.Network, long, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkCapabilities, @NonNull android.os.PersistableBundle);
+    method public int describeContents();
+    method @NonNull public android.os.PersistableBundle getAdditionalInfo();
+    method @NonNull public android.net.LinkProperties getLinkProperties();
+    method @NonNull public android.net.Network getNetwork();
+    method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities();
+    method public long getReportTimestamp();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.ConnectivityDiagnosticsManager.ConnectivityReport> CREATOR;
+    field public static final String KEY_NETWORK_PROBES_ATTEMPTED_BITMASK = "networkProbesAttempted";
+    field public static final String KEY_NETWORK_PROBES_SUCCEEDED_BITMASK = "networkProbesSucceeded";
+    field public static final String KEY_NETWORK_VALIDATION_RESULT = "networkValidationResult";
+    field public static final int NETWORK_PROBE_DNS = 4; // 0x4
+    field public static final int NETWORK_PROBE_FALLBACK = 32; // 0x20
+    field public static final int NETWORK_PROBE_HTTP = 8; // 0x8
+    field public static final int NETWORK_PROBE_HTTPS = 16; // 0x10
+    field public static final int NETWORK_PROBE_PRIVATE_DNS = 64; // 0x40
+    field public static final int NETWORK_VALIDATION_RESULT_INVALID = 0; // 0x0
+    field public static final int NETWORK_VALIDATION_RESULT_PARTIALLY_VALID = 2; // 0x2
+    field public static final int NETWORK_VALIDATION_RESULT_SKIPPED = 3; // 0x3
+    field public static final int NETWORK_VALIDATION_RESULT_VALID = 1; // 0x1
+  }
+
+  public static final class ConnectivityDiagnosticsManager.DataStallReport implements android.os.Parcelable {
+    ctor public ConnectivityDiagnosticsManager.DataStallReport(@NonNull android.net.Network, long, int, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkCapabilities, @NonNull android.os.PersistableBundle);
+    method public int describeContents();
+    method public int getDetectionMethod();
+    method @NonNull public android.net.LinkProperties getLinkProperties();
+    method @NonNull public android.net.Network getNetwork();
+    method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities();
+    method public long getReportTimestamp();
+    method @NonNull public android.os.PersistableBundle getStallDetails();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.ConnectivityDiagnosticsManager.DataStallReport> CREATOR;
+    field public static final int DETECTION_METHOD_DNS_EVENTS = 1; // 0x1
+    field public static final int DETECTION_METHOD_TCP_METRICS = 2; // 0x2
+    field public static final String KEY_DNS_CONSECUTIVE_TIMEOUTS = "dnsConsecutiveTimeouts";
+    field public static final String KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS = "tcpMetricsCollectionPeriodMillis";
+    field public static final String KEY_TCP_PACKET_FAIL_RATE = "tcpPacketFailRate";
+  }
+
+  public class ConnectivityManager {
+    method public void addDefaultNetworkActiveListener(android.net.ConnectivityManager.OnNetworkActiveListener);
+    method public boolean bindProcessToNetwork(@Nullable android.net.Network);
+    method @NonNull public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull android.net.IpSecManager.UdpEncapsulationSocket, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback);
+    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.Network getActiveNetwork();
+    method @Nullable @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public android.net.Network getActiveNetworkForUid(int);
+    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getActiveNetworkInfo();
+    method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo[] getAllNetworkInfo();
+    method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.Network[] getAllNetworks();
+    method @Deprecated public boolean getBackgroundDataSetting();
+    method @Nullable public android.net.Network getBoundNetworkForProcess();
+    method public int getConnectionOwnerUid(int, @NonNull java.net.InetSocketAddress, @NonNull java.net.InetSocketAddress);
+    method @Nullable public android.net.ProxyInfo getDefaultProxy();
+    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.LinkProperties getLinkProperties(@Nullable android.net.Network);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public int getMultipathPreference(@Nullable android.net.Network);
+    method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkCapabilities getNetworkCapabilities(@Nullable android.net.Network);
+    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getNetworkInfo(int);
+    method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getNetworkInfo(@Nullable android.net.Network);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public int getNetworkPreference();
+    method @Nullable public byte[] getNetworkWatchlistConfigHash();
+    method @Deprecated @Nullable public static android.net.Network getProcessDefaultNetwork();
+    method public int getRestrictBackgroundStatus();
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public boolean isActiveNetworkMetered();
+    method public boolean isDefaultNetworkActive();
+    method @Deprecated public static boolean isNetworkTypeValid(int);
+    method public void registerBestMatchingNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
+    method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent);
+    method public void releaseNetworkRequest(@NonNull android.app.PendingIntent);
+    method public void removeDefaultNetworkActiveListener(@NonNull android.net.ConnectivityManager.OnNetworkActiveListener);
+    method @Deprecated public void reportBadNetwork(@Nullable android.net.Network);
+    method public void reportNetworkConnectivity(@Nullable android.net.Network, boolean);
+    method public boolean requestBandwidthUpdate(@NonNull android.net.Network);
+    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback);
+    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
+    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, int);
+    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler, int);
+    method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent);
+    method @Deprecated public void setNetworkPreference(int);
+    method @Deprecated public static boolean setProcessDefaultNetwork(@Nullable android.net.Network);
+    method public void unregisterNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback);
+    method public void unregisterNetworkCallback(@NonNull android.app.PendingIntent);
+    field @Deprecated public static final String ACTION_BACKGROUND_DATA_SETTING_CHANGED = "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED";
+    field public static final String ACTION_CAPTIVE_PORTAL_SIGN_IN = "android.net.conn.CAPTIVE_PORTAL";
+    field public static final String ACTION_RESTRICT_BACKGROUND_CHANGED = "android.net.conn.RESTRICT_BACKGROUND_CHANGED";
+    field @Deprecated public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
+    field @Deprecated public static final int DEFAULT_NETWORK_PREFERENCE = 1; // 0x1
+    field public static final String EXTRA_CAPTIVE_PORTAL = "android.net.extra.CAPTIVE_PORTAL";
+    field public static final String EXTRA_CAPTIVE_PORTAL_URL = "android.net.extra.CAPTIVE_PORTAL_URL";
+    field @Deprecated public static final String EXTRA_EXTRA_INFO = "extraInfo";
+    field @Deprecated public static final String EXTRA_IS_FAILOVER = "isFailover";
+    field public static final String EXTRA_NETWORK = "android.net.extra.NETWORK";
+    field @Deprecated public static final String EXTRA_NETWORK_INFO = "networkInfo";
+    field public static final String EXTRA_NETWORK_REQUEST = "android.net.extra.NETWORK_REQUEST";
+    field @Deprecated public static final String EXTRA_NETWORK_TYPE = "networkType";
+    field public static final String EXTRA_NO_CONNECTIVITY = "noConnectivity";
+    field @Deprecated public static final String EXTRA_OTHER_NETWORK_INFO = "otherNetwork";
+    field public static final String EXTRA_REASON = "reason";
+    field public static final int MULTIPATH_PREFERENCE_HANDOVER = 1; // 0x1
+    field public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 4; // 0x4
+    field public static final int MULTIPATH_PREFERENCE_RELIABILITY = 2; // 0x2
+    field public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1; // 0x1
+    field public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3; // 0x3
+    field public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2; // 0x2
+    field @Deprecated public static final int TYPE_BLUETOOTH = 7; // 0x7
+    field @Deprecated public static final int TYPE_DUMMY = 8; // 0x8
+    field @Deprecated public static final int TYPE_ETHERNET = 9; // 0x9
+    field @Deprecated public static final int TYPE_MOBILE = 0; // 0x0
+    field @Deprecated public static final int TYPE_MOBILE_DUN = 4; // 0x4
+    field @Deprecated public static final int TYPE_MOBILE_HIPRI = 5; // 0x5
+    field @Deprecated public static final int TYPE_MOBILE_MMS = 2; // 0x2
+    field @Deprecated public static final int TYPE_MOBILE_SUPL = 3; // 0x3
+    field @Deprecated public static final int TYPE_VPN = 17; // 0x11
+    field @Deprecated public static final int TYPE_WIFI = 1; // 0x1
+    field @Deprecated public static final int TYPE_WIMAX = 6; // 0x6
+  }
+
+  public static class ConnectivityManager.NetworkCallback {
+    ctor public ConnectivityManager.NetworkCallback();
+    ctor public ConnectivityManager.NetworkCallback(int);
+    method public void onAvailable(@NonNull android.net.Network);
+    method public void onBlockedStatusChanged(@NonNull android.net.Network, boolean);
+    method public void onCapabilitiesChanged(@NonNull android.net.Network, @NonNull android.net.NetworkCapabilities);
+    method public void onLinkPropertiesChanged(@NonNull android.net.Network, @NonNull android.net.LinkProperties);
+    method public void onLosing(@NonNull android.net.Network, int);
+    method public void onLost(@NonNull android.net.Network);
+    method public void onUnavailable();
+    field public static final int FLAG_INCLUDE_LOCATION_INFO = 1; // 0x1
+  }
+
+  public static interface ConnectivityManager.OnNetworkActiveListener {
+    method public void onNetworkActive();
+  }
+
+  public class DhcpInfo implements android.os.Parcelable {
+    ctor public DhcpInfo();
+    method public int describeContents();
+    method public void writeToParcel(android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.DhcpInfo> CREATOR;
+    field public int dns1;
+    field public int dns2;
+    field public int gateway;
+    field public int ipAddress;
+    field public int leaseDuration;
+    field public int netmask;
+    field public int serverAddress;
+  }
+
+  public final class DnsResolver {
+    method @NonNull public static android.net.DnsResolver getInstance();
+    method public void query(@Nullable android.net.Network, @NonNull String, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super java.util.List<java.net.InetAddress>>);
+    method public void query(@Nullable android.net.Network, @NonNull String, int, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super java.util.List<java.net.InetAddress>>);
+    method public void rawQuery(@Nullable android.net.Network, @NonNull byte[], int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super byte[]>);
+    method public void rawQuery(@Nullable android.net.Network, @NonNull String, int, int, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super byte[]>);
+    field public static final int CLASS_IN = 1; // 0x1
+    field public static final int ERROR_PARSE = 0; // 0x0
+    field public static final int ERROR_SYSTEM = 1; // 0x1
+    field public static final int FLAG_EMPTY = 0; // 0x0
+    field public static final int FLAG_NO_CACHE_LOOKUP = 4; // 0x4
+    field public static final int FLAG_NO_CACHE_STORE = 2; // 0x2
+    field public static final int FLAG_NO_RETRY = 1; // 0x1
+    field public static final int TYPE_A = 1; // 0x1
+    field public static final int TYPE_AAAA = 28; // 0x1c
+  }
+
+  public static interface DnsResolver.Callback<T> {
+    method public void onAnswer(@NonNull T, int);
+    method public void onError(@NonNull android.net.DnsResolver.DnsException);
+  }
+
+  public static class DnsResolver.DnsException extends java.lang.Exception {
+    field public final int code;
+  }
+
+  public class InetAddresses {
+    method public static boolean isNumericAddress(@NonNull String);
+    method @NonNull public static java.net.InetAddress parseNumericAddress(@NonNull String);
+  }
+
+  public final class IpPrefix implements android.os.Parcelable {
+    method public boolean contains(@NonNull java.net.InetAddress);
+    method public int describeContents();
+    method @NonNull public java.net.InetAddress getAddress();
+    method @IntRange(from=0, to=128) public int getPrefixLength();
+    method @NonNull public byte[] getRawAddress();
+    method public void writeToParcel(android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.IpPrefix> CREATOR;
+  }
+
+  public class LinkAddress implements android.os.Parcelable {
+    method public int describeContents();
+    method public java.net.InetAddress getAddress();
+    method public int getFlags();
+    method @IntRange(from=0, to=128) public int getPrefixLength();
+    method public int getScope();
+    method public void writeToParcel(android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.LinkAddress> CREATOR;
+  }
+
+  public final class LinkProperties implements android.os.Parcelable {
+    ctor public LinkProperties();
+    method public boolean addRoute(@NonNull android.net.RouteInfo);
+    method public void clear();
+    method public int describeContents();
+    method @Nullable public java.net.Inet4Address getDhcpServerAddress();
+    method @NonNull public java.util.List<java.net.InetAddress> getDnsServers();
+    method @Nullable public String getDomains();
+    method @Nullable public android.net.ProxyInfo getHttpProxy();
+    method @Nullable public String getInterfaceName();
+    method @NonNull public java.util.List<android.net.LinkAddress> getLinkAddresses();
+    method public int getMtu();
+    method @Nullable public android.net.IpPrefix getNat64Prefix();
+    method @Nullable public String getPrivateDnsServerName();
+    method @NonNull public java.util.List<android.net.RouteInfo> getRoutes();
+    method public boolean isPrivateDnsActive();
+    method public boolean isWakeOnLanSupported();
+    method public void setDhcpServerAddress(@Nullable java.net.Inet4Address);
+    method public void setDnsServers(@NonNull java.util.Collection<java.net.InetAddress>);
+    method public void setDomains(@Nullable String);
+    method public void setHttpProxy(@Nullable android.net.ProxyInfo);
+    method public void setInterfaceName(@Nullable String);
+    method public void setLinkAddresses(@NonNull java.util.Collection<android.net.LinkAddress>);
+    method public void setMtu(int);
+    method public void setNat64Prefix(@Nullable android.net.IpPrefix);
+    method public void writeToParcel(android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.LinkProperties> CREATOR;
+  }
+
+  public final class MacAddress implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public static android.net.MacAddress fromBytes(@NonNull byte[]);
+    method @NonNull public static android.net.MacAddress fromString(@NonNull String);
+    method public int getAddressType();
+    method @Nullable public java.net.Inet6Address getLinkLocalIpv6FromEui48Mac();
+    method public boolean isLocallyAssigned();
+    method public boolean matches(@NonNull android.net.MacAddress, @NonNull android.net.MacAddress);
+    method @NonNull public byte[] toByteArray();
+    method @NonNull public String toOuiString();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.net.MacAddress BROADCAST_ADDRESS;
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.MacAddress> CREATOR;
+    field public static final int TYPE_BROADCAST = 3; // 0x3
+    field public static final int TYPE_MULTICAST = 2; // 0x2
+    field public static final int TYPE_UNICAST = 1; // 0x1
+  }
+
+  public class Network implements android.os.Parcelable {
+    method public void bindSocket(java.net.DatagramSocket) throws java.io.IOException;
+    method public void bindSocket(java.net.Socket) throws java.io.IOException;
+    method public void bindSocket(java.io.FileDescriptor) throws java.io.IOException;
+    method public int describeContents();
+    method public static android.net.Network fromNetworkHandle(long);
+    method public java.net.InetAddress[] getAllByName(String) throws java.net.UnknownHostException;
+    method public java.net.InetAddress getByName(String) throws java.net.UnknownHostException;
+    method public long getNetworkHandle();
+    method public javax.net.SocketFactory getSocketFactory();
+    method public java.net.URLConnection openConnection(java.net.URL) throws java.io.IOException;
+    method public java.net.URLConnection openConnection(java.net.URL, java.net.Proxy) throws java.io.IOException;
+    method public void writeToParcel(android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.Network> CREATOR;
+  }
+
+  public final class NetworkCapabilities implements android.os.Parcelable {
+    ctor public NetworkCapabilities();
+    ctor public NetworkCapabilities(android.net.NetworkCapabilities);
+    method public int describeContents();
+    method @NonNull public int[] getCapabilities();
+    method public int getLinkDownstreamBandwidthKbps();
+    method public int getLinkUpstreamBandwidthKbps();
+    method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
+    method public int getOwnerUid();
+    method public int getSignalStrength();
+    method @Nullable public android.net.TransportInfo getTransportInfo();
+    method public boolean hasCapability(int);
+    method public boolean hasTransport(int);
+    method public void writeToParcel(android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkCapabilities> CREATOR;
+    field public static final int NET_CAPABILITY_CAPTIVE_PORTAL = 17; // 0x11
+    field public static final int NET_CAPABILITY_CBS = 5; // 0x5
+    field public static final int NET_CAPABILITY_DUN = 2; // 0x2
+    field public static final int NET_CAPABILITY_EIMS = 10; // 0xa
+    field public static final int NET_CAPABILITY_ENTERPRISE = 29; // 0x1d
+    field public static final int NET_CAPABILITY_FOREGROUND = 19; // 0x13
+    field public static final int NET_CAPABILITY_FOTA = 3; // 0x3
+    field public static final int NET_CAPABILITY_IA = 7; // 0x7
+    field public static final int NET_CAPABILITY_IMS = 4; // 0x4
+    field public static final int NET_CAPABILITY_INTERNET = 12; // 0xc
+    field public static final int NET_CAPABILITY_MCX = 23; // 0x17
+    field public static final int NET_CAPABILITY_MMS = 0; // 0x0
+    field public static final int NET_CAPABILITY_NOT_CONGESTED = 20; // 0x14
+    field public static final int NET_CAPABILITY_NOT_METERED = 11; // 0xb
+    field public static final int NET_CAPABILITY_NOT_RESTRICTED = 13; // 0xd
+    field public static final int NET_CAPABILITY_NOT_ROAMING = 18; // 0x12
+    field public static final int NET_CAPABILITY_NOT_SUSPENDED = 21; // 0x15
+    field public static final int NET_CAPABILITY_NOT_VPN = 15; // 0xf
+    field public static final int NET_CAPABILITY_RCS = 8; // 0x8
+    field public static final int NET_CAPABILITY_SUPL = 1; // 0x1
+    field public static final int NET_CAPABILITY_TEMPORARILY_NOT_METERED = 25; // 0x19
+    field public static final int NET_CAPABILITY_TRUSTED = 14; // 0xe
+    field public static final int NET_CAPABILITY_VALIDATED = 16; // 0x10
+    field public static final int NET_CAPABILITY_WIFI_P2P = 6; // 0x6
+    field public static final int NET_CAPABILITY_XCAP = 9; // 0x9
+    field public static final int SIGNAL_STRENGTH_UNSPECIFIED = -2147483648; // 0x80000000
+    field public static final int TRANSPORT_BLUETOOTH = 2; // 0x2
+    field public static final int TRANSPORT_CELLULAR = 0; // 0x0
+    field public static final int TRANSPORT_ETHERNET = 3; // 0x3
+    field public static final int TRANSPORT_LOWPAN = 6; // 0x6
+    field public static final int TRANSPORT_VPN = 4; // 0x4
+    field public static final int TRANSPORT_WIFI = 1; // 0x1
+    field public static final int TRANSPORT_WIFI_AWARE = 5; // 0x5
+  }
+
+  @Deprecated public class NetworkInfo implements android.os.Parcelable {
+    ctor @Deprecated public NetworkInfo(int, int, @Nullable String, @Nullable String);
+    method @Deprecated public int describeContents();
+    method @Deprecated @NonNull public android.net.NetworkInfo.DetailedState getDetailedState();
+    method @Deprecated public String getExtraInfo();
+    method @Deprecated public String getReason();
+    method @Deprecated public android.net.NetworkInfo.State getState();
+    method @Deprecated public int getSubtype();
+    method @Deprecated public String getSubtypeName();
+    method @Deprecated public int getType();
+    method @Deprecated public String getTypeName();
+    method @Deprecated public boolean isAvailable();
+    method @Deprecated public boolean isConnected();
+    method @Deprecated public boolean isConnectedOrConnecting();
+    method @Deprecated public boolean isFailover();
+    method @Deprecated public boolean isRoaming();
+    method @Deprecated public void setDetailedState(@NonNull android.net.NetworkInfo.DetailedState, @Nullable String, @Nullable String);
+    method @Deprecated public void writeToParcel(android.os.Parcel, int);
+    field @Deprecated @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkInfo> CREATOR;
+  }
+
+  @Deprecated public enum NetworkInfo.DetailedState {
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState AUTHENTICATING;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState BLOCKED;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CAPTIVE_PORTAL_CHECK;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CONNECTED;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CONNECTING;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState DISCONNECTED;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState DISCONNECTING;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState FAILED;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState IDLE;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState OBTAINING_IPADDR;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState SCANNING;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState SUSPENDED;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState VERIFYING_POOR_LINK;
+  }
+
+  @Deprecated public enum NetworkInfo.State {
+    enum_constant @Deprecated public static final android.net.NetworkInfo.State CONNECTED;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.State CONNECTING;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.State DISCONNECTED;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.State DISCONNECTING;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.State SUSPENDED;
+    enum_constant @Deprecated public static final android.net.NetworkInfo.State UNKNOWN;
+  }
+
+  public class NetworkRequest implements android.os.Parcelable {
+    method public boolean canBeSatisfiedBy(@Nullable android.net.NetworkCapabilities);
+    method public int describeContents();
+    method @NonNull public int[] getCapabilities();
+    method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
+    method @NonNull public int[] getTransportTypes();
+    method public boolean hasCapability(int);
+    method public boolean hasTransport(int);
+    method public void writeToParcel(android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkRequest> CREATOR;
+  }
+
+  public static class NetworkRequest.Builder {
+    ctor public NetworkRequest.Builder();
+    ctor public NetworkRequest.Builder(@NonNull android.net.NetworkRequest);
+    method public android.net.NetworkRequest.Builder addCapability(int);
+    method public android.net.NetworkRequest.Builder addTransportType(int);
+    method public android.net.NetworkRequest build();
+    method @NonNull public android.net.NetworkRequest.Builder clearCapabilities();
+    method public android.net.NetworkRequest.Builder removeCapability(int);
+    method public android.net.NetworkRequest.Builder removeTransportType(int);
+    method @Deprecated public android.net.NetworkRequest.Builder setNetworkSpecifier(String);
+    method public android.net.NetworkRequest.Builder setNetworkSpecifier(android.net.NetworkSpecifier);
+  }
+
+  public class ParseException extends java.lang.RuntimeException {
+    ctor public ParseException(@NonNull String);
+    ctor public ParseException(@NonNull String, @NonNull Throwable);
+    field public String response;
+  }
+
+  public class ProxyInfo implements android.os.Parcelable {
+    ctor public ProxyInfo(@Nullable android.net.ProxyInfo);
+    method public static android.net.ProxyInfo buildDirectProxy(String, int);
+    method public static android.net.ProxyInfo buildDirectProxy(String, int, java.util.List<java.lang.String>);
+    method public static android.net.ProxyInfo buildPacProxy(android.net.Uri);
+    method @NonNull public static android.net.ProxyInfo buildPacProxy(@NonNull android.net.Uri, int);
+    method public int describeContents();
+    method public String[] getExclusionList();
+    method public String getHost();
+    method public android.net.Uri getPacFileUrl();
+    method public int getPort();
+    method public boolean isValid();
+    method public void writeToParcel(android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.ProxyInfo> CREATOR;
+  }
+
+  public final class RouteInfo implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public android.net.IpPrefix getDestination();
+    method @Nullable public java.net.InetAddress getGateway();
+    method @Nullable public String getInterface();
+    method public boolean hasGateway();
+    method public boolean isDefaultRoute();
+    method public boolean matches(java.net.InetAddress);
+    method public void writeToParcel(android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.RouteInfo> CREATOR;
+  }
+
+  public abstract class SocketKeepalive implements java.lang.AutoCloseable {
+    method public final void close();
+    method public final void start(@IntRange(from=0xa, to=0xe10) int);
+    method public final void stop();
+    field public static final int ERROR_HARDWARE_ERROR = -31; // 0xffffffe1
+    field public static final int ERROR_INSUFFICIENT_RESOURCES = -32; // 0xffffffe0
+    field public static final int ERROR_INVALID_INTERVAL = -24; // 0xffffffe8
+    field public static final int ERROR_INVALID_IP_ADDRESS = -21; // 0xffffffeb
+    field public static final int ERROR_INVALID_LENGTH = -23; // 0xffffffe9
+    field public static final int ERROR_INVALID_NETWORK = -20; // 0xffffffec
+    field public static final int ERROR_INVALID_PORT = -22; // 0xffffffea
+    field public static final int ERROR_INVALID_SOCKET = -25; // 0xffffffe7
+    field public static final int ERROR_SOCKET_NOT_IDLE = -26; // 0xffffffe6
+    field public static final int ERROR_UNSUPPORTED = -30; // 0xffffffe2
+  }
+
+  public static class SocketKeepalive.Callback {
+    ctor public SocketKeepalive.Callback();
+    method public void onDataReceived();
+    method public void onError(int);
+    method public void onStarted();
+    method public void onStopped();
+  }
+
+  public interface TransportInfo {
+  }
+
+}
+
diff --git a/framework/api/lint-baseline.txt b/framework/api/lint-baseline.txt
new file mode 100644
index 0000000..2f4004a
--- /dev/null
+++ b/framework/api/lint-baseline.txt
@@ -0,0 +1,4 @@
+// Baseline format: 1.0
+VisiblySynchronized: android.net.NetworkInfo#toString():
+    Internal locks must not be exposed (synchronizing on this or class is still
+    externally observable): method android.net.NetworkInfo.toString()
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
new file mode 100644
index 0000000..7fc0382
--- /dev/null
+++ b/framework/api/module-lib-current.txt
@@ -0,0 +1,179 @@
+// Signature format: 2.0
+package android.net {
+
+  public final class ConnectivityFrameworkInitializer {
+    method public static void registerServiceWrappers();
+  }
+
+  public class ConnectivityManager {
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void factoryReset();
+    method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public java.util.List<android.net.NetworkStateSnapshot> getAllNetworkStateSnapshots();
+    method @Nullable public android.net.ProxyInfo getGlobalProxy();
+    method @NonNull public static android.util.Range<java.lang.Integer> getIpSecNetIdRange();
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerDefaultNetworkCallbackForUid(int, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerSystemDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void requestBackgroundNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
+    method @Deprecated public boolean requestRouteToHostAddress(int, java.net.InetAddress);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptPartialConnectivity(@NonNull android.net.Network, boolean, boolean);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptUnvalidated(@NonNull android.net.Network, boolean, boolean);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAvoidUnvalidated(@NonNull android.net.Network);
+    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setGlobalProxy(@Nullable android.net.ProxyInfo);
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setLegacyLockdownVpnEnabled(boolean);
+    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreference(@NonNull android.os.UserHandle, int, @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.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();
+    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";
+    field public static final String ACTION_PROMPT_UNVALIDATED = "android.net.action.PROMPT_UNVALIDATED";
+    field public static final int BLOCKED_METERED_REASON_ADMIN_DISABLED = 262144; // 0x40000
+    field public static final int BLOCKED_METERED_REASON_DATA_SAVER = 65536; // 0x10000
+    field public static final int BLOCKED_METERED_REASON_MASK = -65536; // 0xffff0000
+    field public static final int BLOCKED_METERED_REASON_USER_RESTRICTED = 131072; // 0x20000
+    field public static final int BLOCKED_REASON_APP_STANDBY = 4; // 0x4
+    field public static final int BLOCKED_REASON_BATTERY_SAVER = 1; // 0x1
+    field public static final int BLOCKED_REASON_DOZE = 2; // 0x2
+    field public static final int BLOCKED_REASON_LOCKDOWN_VPN = 16; // 0x10
+    field public static final int BLOCKED_REASON_NONE = 0; // 0x0
+    field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8
+    field public static final int PROFILE_NETWORK_PREFERENCE_DEFAULT = 0; // 0x0
+    field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE = 1; // 0x1
+  }
+
+  public static class ConnectivityManager.NetworkCallback {
+    method public void onBlockedStatusChanged(@NonNull android.net.Network, int);
+  }
+
+  public class ConnectivitySettingsManager {
+    method public static void clearGlobalProxy(@NonNull android.content.Context);
+    method @Nullable public static String getCaptivePortalHttpUrl(@NonNull android.content.Context);
+    method public static int getCaptivePortalMode(@NonNull android.content.Context, int);
+    method @NonNull public static java.time.Duration getConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
+    method @NonNull public static android.util.Range<java.lang.Integer> getDnsResolverSampleRanges(@NonNull android.content.Context);
+    method @NonNull public static java.time.Duration getDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
+    method public static int getDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, int);
+    method @Nullable public static android.net.ProxyInfo getGlobalProxy(@NonNull android.content.Context);
+    method @NonNull public static java.time.Duration getMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
+    method public static boolean getMobileDataAlwaysOn(@NonNull android.content.Context, boolean);
+    method @NonNull public static java.util.Set<java.lang.Integer> getMobileDataPreferredUids(@NonNull android.content.Context);
+    method public static int getNetworkAvoidBadWifi(@NonNull android.content.Context);
+    method @Nullable public static String getNetworkMeteredMultipathPreference(@NonNull android.content.Context);
+    method public static int getNetworkSwitchNotificationMaximumDailyCount(@NonNull android.content.Context, int);
+    method @NonNull public static java.time.Duration getNetworkSwitchNotificationRateDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
+    method @NonNull public static String getPrivateDnsDefaultMode(@NonNull android.content.Context);
+    method @Nullable public static String getPrivateDnsHostname(@NonNull android.content.Context);
+    method public static int getPrivateDnsMode(@NonNull android.content.Context);
+    method @NonNull public static java.util.Set<java.lang.Integer> getUidsAllowedOnRestrictedNetworks(@NonNull android.content.Context);
+    method public static boolean getWifiAlwaysRequested(@NonNull android.content.Context, boolean);
+    method @NonNull public static java.time.Duration getWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
+    method public static void setCaptivePortalHttpUrl(@NonNull android.content.Context, @Nullable String);
+    method public static void setCaptivePortalMode(@NonNull android.content.Context, int);
+    method public static void setConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
+    method public static void setDnsResolverSampleRanges(@NonNull android.content.Context, @NonNull android.util.Range<java.lang.Integer>);
+    method public static void setDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
+    method public static void setDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, @IntRange(from=0, to=100) int);
+    method public static void setGlobalProxy(@NonNull android.content.Context, @NonNull android.net.ProxyInfo);
+    method public static void setMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
+    method public static void setMobileDataAlwaysOn(@NonNull android.content.Context, boolean);
+    method public static void setMobileDataPreferredUids(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.Integer>);
+    method public static void setNetworkAvoidBadWifi(@NonNull android.content.Context, int);
+    method public static void setNetworkMeteredMultipathPreference(@NonNull android.content.Context, @NonNull String);
+    method public static void setNetworkSwitchNotificationMaximumDailyCount(@NonNull android.content.Context, @IntRange(from=0) int);
+    method public static void setNetworkSwitchNotificationRateDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
+    method public static void setPrivateDnsDefaultMode(@NonNull android.content.Context, @NonNull int);
+    method public static void setPrivateDnsHostname(@NonNull android.content.Context, @Nullable String);
+    method public static void setPrivateDnsMode(@NonNull android.content.Context, int);
+    method public static void setUidsAllowedOnRestrictedNetworks(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.Integer>);
+    method public static void setWifiAlwaysRequested(@NonNull android.content.Context, boolean);
+    method public static void setWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
+    field public static final int CAPTIVE_PORTAL_MODE_AVOID = 2; // 0x2
+    field public static final int CAPTIVE_PORTAL_MODE_IGNORE = 0; // 0x0
+    field public static final int CAPTIVE_PORTAL_MODE_PROMPT = 1; // 0x1
+    field public static final int NETWORK_AVOID_BAD_WIFI_AVOID = 2; // 0x2
+    field public static final int NETWORK_AVOID_BAD_WIFI_IGNORE = 0; // 0x0
+    field public static final int NETWORK_AVOID_BAD_WIFI_PROMPT = 1; // 0x1
+    field public static final int PRIVATE_DNS_MODE_OFF = 1; // 0x1
+    field public static final int PRIVATE_DNS_MODE_OPPORTUNISTIC = 2; // 0x2
+    field public static final int PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = 3; // 0x3
+  }
+
+  public final class NetworkAgentConfig implements android.os.Parcelable {
+    method @Nullable public String getSubscriberId();
+    method public boolean isBypassableVpn();
+  }
+
+  public static final class NetworkAgentConfig.Builder {
+    method @NonNull public android.net.NetworkAgentConfig.Builder setBypassableVpn(boolean);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setSubscriberId(@Nullable String);
+  }
+
+  public final class NetworkCapabilities implements android.os.Parcelable {
+    method @Nullable public java.util.Set<android.util.Range<java.lang.Integer>> getUids();
+    method public boolean hasForbiddenCapability(int);
+    field public static final long REDACT_ALL = -1L; // 0xffffffffffffffffL
+    field public static final long REDACT_FOR_ACCESS_FINE_LOCATION = 1L; // 0x1L
+    field public static final long REDACT_FOR_LOCAL_MAC_ADDRESS = 2L; // 0x2L
+    field public static final long REDACT_FOR_NETWORK_SETTINGS = 4L; // 0x4L
+    field public static final long REDACT_NONE = 0L; // 0x0L
+    field public static final int TRANSPORT_TEST = 7; // 0x7
+  }
+
+  public static final class NetworkCapabilities.Builder {
+    method @NonNull public android.net.NetworkCapabilities.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>);
+  }
+
+  public class NetworkRequest implements android.os.Parcelable {
+    method @NonNull public int[] getForbiddenCapabilities();
+    method public boolean hasForbiddenCapability(int);
+  }
+
+  public static class NetworkRequest.Builder {
+    method @NonNull public android.net.NetworkRequest.Builder addForbiddenCapability(int);
+    method @NonNull public android.net.NetworkRequest.Builder removeForbiddenCapability(int);
+    method @NonNull public android.net.NetworkRequest.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>);
+  }
+
+  public final class TestNetworkInterface implements android.os.Parcelable {
+    ctor public TestNetworkInterface(@NonNull android.os.ParcelFileDescriptor, @NonNull String);
+    method public int describeContents();
+    method @NonNull public android.os.ParcelFileDescriptor getFileDescriptor();
+    method @NonNull public String getInterfaceName();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkInterface> CREATOR;
+  }
+
+  public class TestNetworkManager {
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public android.net.TestNetworkInterface createTapInterface();
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public android.net.TestNetworkInterface createTunInterface(@NonNull java.util.Collection<android.net.LinkAddress>);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public void setupTestNetwork(@NonNull String, @NonNull android.os.IBinder);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public void teardownTestNetwork(@NonNull android.net.Network);
+    field public static final String TEST_TAP_PREFIX = "testtap";
+  }
+
+  public final class TestNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable {
+    ctor public TestNetworkSpecifier(@NonNull String);
+    method public int describeContents();
+    method @Nullable public String getInterfaceName();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkSpecifier> CREATOR;
+  }
+
+  public interface TransportInfo {
+    method public default long getApplicableRedactions();
+    method @NonNull public default android.net.TransportInfo makeCopy(long);
+  }
+
+  public final class VpnTransportInfo implements android.os.Parcelable android.net.TransportInfo {
+    ctor public VpnTransportInfo(int, @Nullable String);
+    method public int describeContents();
+    method @Nullable public String getSessionId();
+    method public int getType();
+    method @NonNull public android.net.VpnTransportInfo makeCopy(long);
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.VpnTransportInfo> CREATOR;
+  }
+
+}
+
diff --git a/framework/api/module-lib-removed.txt b/framework/api/module-lib-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/framework/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/framework/api/removed.txt b/framework/api/removed.txt
new file mode 100644
index 0000000..303a1e6
--- /dev/null
+++ b/framework/api/removed.txt
@@ -0,0 +1,11 @@
+// Signature format: 2.0
+package android.net {
+
+  public class ConnectivityManager {
+    method @Deprecated public boolean requestRouteToHost(int, int);
+    method @Deprecated public int startUsingNetworkFeature(int, String);
+    method @Deprecated public int stopUsingNetworkFeature(int, String);
+  }
+
+}
+
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
new file mode 100644
index 0000000..d1d51da
--- /dev/null
+++ b/framework/api/system-current.txt
@@ -0,0 +1,505 @@
+// Signature format: 2.0
+package android.net {
+
+  public class CaptivePortal implements android.os.Parcelable {
+    method @Deprecated public void logEvent(int, @NonNull String);
+    method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void reevaluateNetwork();
+    method public void useNetwork();
+    field public static final int APP_REQUEST_REEVALUATION_REQUIRED = 100; // 0x64
+    field public static final int APP_RETURN_DISMISSED = 0; // 0x0
+    field public static final int APP_RETURN_UNWANTED = 1; // 0x1
+    field public static final int APP_RETURN_WANTED_AS_IS = 2; // 0x2
+  }
+
+  public final class CaptivePortalData implements android.os.Parcelable {
+    method public int describeContents();
+    method public long getByteLimit();
+    method public long getExpiryTimeMillis();
+    method public long getRefreshTimeMillis();
+    method @Nullable public android.net.Uri getUserPortalUrl();
+    method public int getUserPortalUrlSource();
+    method @Nullable public CharSequence getVenueFriendlyName();
+    method @Nullable public android.net.Uri getVenueInfoUrl();
+    method public int getVenueInfoUrlSource();
+    method public boolean isCaptive();
+    method public boolean isSessionExtendable();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field public static final int CAPTIVE_PORTAL_DATA_SOURCE_OTHER = 0; // 0x0
+    field public static final int CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT = 1; // 0x1
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.CaptivePortalData> CREATOR;
+  }
+
+  public static class CaptivePortalData.Builder {
+    ctor public CaptivePortalData.Builder();
+    ctor public CaptivePortalData.Builder(@Nullable android.net.CaptivePortalData);
+    method @NonNull public android.net.CaptivePortalData build();
+    method @NonNull public android.net.CaptivePortalData.Builder setBytesRemaining(long);
+    method @NonNull public android.net.CaptivePortalData.Builder setCaptive(boolean);
+    method @NonNull public android.net.CaptivePortalData.Builder setExpiryTime(long);
+    method @NonNull public android.net.CaptivePortalData.Builder setRefreshTime(long);
+    method @NonNull public android.net.CaptivePortalData.Builder setSessionExtendable(boolean);
+    method @NonNull public android.net.CaptivePortalData.Builder setUserPortalUrl(@Nullable android.net.Uri);
+    method @NonNull public android.net.CaptivePortalData.Builder setUserPortalUrl(@Nullable android.net.Uri, int);
+    method @NonNull public android.net.CaptivePortalData.Builder setVenueFriendlyName(@Nullable CharSequence);
+    method @NonNull public android.net.CaptivePortalData.Builder setVenueInfoUrl(@Nullable android.net.Uri);
+    method @NonNull public android.net.CaptivePortalData.Builder setVenueInfoUrl(@Nullable android.net.Uri, int);
+  }
+
+  public class ConnectivityManager {
+    method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createNattKeepalive(@NonNull android.net.Network, @NonNull android.os.ParcelFileDescriptor, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback);
+    method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull java.net.Socket, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public String getCaptivePortalServerUrl();
+    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void getLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityManager.OnTetheringEntitlementResultListener);
+    method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public boolean isTetheringSupported();
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_FACTORY}) public int registerNetworkProvider(@NonNull android.net.NetworkProvider);
+    method public void registerQosCallback(@NonNull android.net.QosSocketInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.QosCallback);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityManager.OnTetheringEventCallback);
+    method @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public void requestNetwork(@NonNull android.net.NetworkRequest, int, int, @NonNull android.os.Handler, @NonNull android.net.ConnectivityManager.NetworkCallback);
+    method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_AIRPLANE_MODE, android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void setAirplaneMode(boolean);
+    method @RequiresPermission(android.Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE) public void setOemNetworkPreference(@NonNull android.net.OemNetworkPreferences, @Nullable java.util.concurrent.Executor, @Nullable Runnable);
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public boolean shouldAvoidBadWifi();
+    method @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public void startCaptivePortalApp(@NonNull android.net.Network, @NonNull android.os.Bundle);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, boolean, android.net.ConnectivityManager.OnStartTetheringCallback);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, boolean, android.net.ConnectivityManager.OnStartTetheringCallback, android.os.Handler);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void stopTethering(int);
+    method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_FACTORY}) public void unregisterNetworkProvider(@NonNull android.net.NetworkProvider);
+    method public void unregisterQosCallback(@NonNull android.net.QosCallback);
+    method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void unregisterTetheringEventCallback(@NonNull android.net.ConnectivityManager.OnTetheringEventCallback);
+    field public static final String EXTRA_CAPTIVE_PORTAL_PROBE_SPEC = "android.net.extra.CAPTIVE_PORTAL_PROBE_SPEC";
+    field public static final String EXTRA_CAPTIVE_PORTAL_USER_AGENT = "android.net.extra.CAPTIVE_PORTAL_USER_AGENT";
+    field public static final int TETHERING_BLUETOOTH = 2; // 0x2
+    field public static final int TETHERING_USB = 1; // 0x1
+    field public static final int TETHERING_WIFI = 0; // 0x0
+    field @Deprecated public static final int TETHER_ERROR_ENTITLEMENT_UNKONWN = 13; // 0xd
+    field @Deprecated public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0
+    field @Deprecated public static final int TETHER_ERROR_PROVISION_FAILED = 11; // 0xb
+    field public static final int TYPE_NONE = -1; // 0xffffffff
+    field @Deprecated public static final int TYPE_PROXY = 16; // 0x10
+    field @Deprecated public static final int TYPE_WIFI_P2P = 13; // 0xd
+  }
+
+  @Deprecated public abstract static class ConnectivityManager.OnStartTetheringCallback {
+    ctor @Deprecated public ConnectivityManager.OnStartTetheringCallback();
+    method @Deprecated public void onTetheringFailed();
+    method @Deprecated public void onTetheringStarted();
+  }
+
+  @Deprecated public static interface ConnectivityManager.OnTetheringEntitlementResultListener {
+    method @Deprecated public void onTetheringEntitlementResult(int);
+  }
+
+  @Deprecated public abstract static class ConnectivityManager.OnTetheringEventCallback {
+    ctor @Deprecated public ConnectivityManager.OnTetheringEventCallback();
+    method @Deprecated public void onUpstreamChanged(@Nullable android.net.Network);
+  }
+
+  public final class InvalidPacketException extends java.lang.Exception {
+    ctor public InvalidPacketException(int);
+    method public int getError();
+    field public static final int ERROR_INVALID_IP_ADDRESS = -21; // 0xffffffeb
+    field public static final int ERROR_INVALID_LENGTH = -23; // 0xffffffe9
+    field public static final int ERROR_INVALID_PORT = -22; // 0xffffffea
+  }
+
+  public final class IpConfiguration implements android.os.Parcelable {
+    ctor public IpConfiguration();
+    ctor public IpConfiguration(@NonNull android.net.IpConfiguration);
+    method public int describeContents();
+    method @Nullable public android.net.ProxyInfo getHttpProxy();
+    method @NonNull public android.net.IpConfiguration.IpAssignment getIpAssignment();
+    method @NonNull public android.net.IpConfiguration.ProxySettings getProxySettings();
+    method @Nullable public android.net.StaticIpConfiguration getStaticIpConfiguration();
+    method public void setHttpProxy(@Nullable android.net.ProxyInfo);
+    method public void setIpAssignment(@NonNull android.net.IpConfiguration.IpAssignment);
+    method public void setProxySettings(@NonNull android.net.IpConfiguration.ProxySettings);
+    method public void setStaticIpConfiguration(@Nullable android.net.StaticIpConfiguration);
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.IpConfiguration> CREATOR;
+  }
+
+  public enum IpConfiguration.IpAssignment {
+    enum_constant public static final android.net.IpConfiguration.IpAssignment DHCP;
+    enum_constant public static final android.net.IpConfiguration.IpAssignment STATIC;
+    enum_constant public static final android.net.IpConfiguration.IpAssignment UNASSIGNED;
+  }
+
+  public enum IpConfiguration.ProxySettings {
+    enum_constant public static final android.net.IpConfiguration.ProxySettings NONE;
+    enum_constant public static final android.net.IpConfiguration.ProxySettings PAC;
+    enum_constant public static final android.net.IpConfiguration.ProxySettings STATIC;
+    enum_constant public static final android.net.IpConfiguration.ProxySettings UNASSIGNED;
+  }
+
+  public final class IpPrefix implements android.os.Parcelable {
+    ctor public IpPrefix(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int);
+    ctor public IpPrefix(@NonNull String);
+  }
+
+  public class KeepalivePacketData {
+    ctor protected KeepalivePacketData(@NonNull java.net.InetAddress, @IntRange(from=0, to=65535) int, @NonNull java.net.InetAddress, @IntRange(from=0, to=65535) int, @NonNull byte[]) throws android.net.InvalidPacketException;
+    method @NonNull public java.net.InetAddress getDstAddress();
+    method public int getDstPort();
+    method @NonNull public byte[] getPacket();
+    method @NonNull public java.net.InetAddress getSrcAddress();
+    method public int getSrcPort();
+  }
+
+  public class LinkAddress implements android.os.Parcelable {
+    ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int, int, int);
+    ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int, int, int, long, long);
+    ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int);
+    ctor public LinkAddress(@NonNull String);
+    ctor public LinkAddress(@NonNull String, int, int);
+    method public long getDeprecationTime();
+    method public long getExpirationTime();
+    method public boolean isGlobalPreferred();
+    method public boolean isIpv4();
+    method public boolean isIpv6();
+    method public boolean isSameAddressAs(@Nullable android.net.LinkAddress);
+    field public static final long LIFETIME_PERMANENT = 9223372036854775807L; // 0x7fffffffffffffffL
+    field public static final long LIFETIME_UNKNOWN = -1L; // 0xffffffffffffffffL
+  }
+
+  public final class LinkProperties implements android.os.Parcelable {
+    ctor public LinkProperties(@Nullable android.net.LinkProperties);
+    ctor public LinkProperties(@Nullable android.net.LinkProperties, boolean);
+    method public boolean addDnsServer(@NonNull java.net.InetAddress);
+    method public boolean addLinkAddress(@NonNull android.net.LinkAddress);
+    method public boolean addPcscfServer(@NonNull java.net.InetAddress);
+    method @NonNull public java.util.List<java.net.InetAddress> getAddresses();
+    method @NonNull public java.util.List<java.lang.String> getAllInterfaceNames();
+    method @NonNull public java.util.List<android.net.LinkAddress> getAllLinkAddresses();
+    method @NonNull public java.util.List<android.net.RouteInfo> getAllRoutes();
+    method @Nullable public android.net.Uri getCaptivePortalApiUrl();
+    method @Nullable public android.net.CaptivePortalData getCaptivePortalData();
+    method @NonNull public java.util.List<java.net.InetAddress> getPcscfServers();
+    method @Nullable public String getTcpBufferSizes();
+    method @NonNull public java.util.List<java.net.InetAddress> getValidatedPrivateDnsServers();
+    method public boolean hasGlobalIpv6Address();
+    method public boolean hasIpv4Address();
+    method public boolean hasIpv4DefaultRoute();
+    method public boolean hasIpv4DnsServer();
+    method public boolean hasIpv6DefaultRoute();
+    method public boolean hasIpv6DnsServer();
+    method public boolean isIpv4Provisioned();
+    method public boolean isIpv6Provisioned();
+    method public boolean isProvisioned();
+    method public boolean isReachable(@NonNull java.net.InetAddress);
+    method public boolean removeDnsServer(@NonNull java.net.InetAddress);
+    method public boolean removeLinkAddress(@NonNull android.net.LinkAddress);
+    method public boolean removeRoute(@NonNull android.net.RouteInfo);
+    method public void setCaptivePortalApiUrl(@Nullable android.net.Uri);
+    method public void setCaptivePortalData(@Nullable android.net.CaptivePortalData);
+    method public void setPcscfServers(@NonNull java.util.Collection<java.net.InetAddress>);
+    method public void setPrivateDnsServerName(@Nullable String);
+    method public void setTcpBufferSizes(@Nullable String);
+    method public void setUsePrivateDns(boolean);
+    method public void setValidatedPrivateDnsServers(@NonNull java.util.Collection<java.net.InetAddress>);
+  }
+
+  public final class NattKeepalivePacketData extends android.net.KeepalivePacketData implements android.os.Parcelable {
+    ctor public NattKeepalivePacketData(@NonNull java.net.InetAddress, int, @NonNull java.net.InetAddress, int, @NonNull byte[]) throws android.net.InvalidPacketException;
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.NattKeepalivePacketData> CREATOR;
+  }
+
+  public class Network implements android.os.Parcelable {
+    ctor public Network(@NonNull android.net.Network);
+    method public int getNetId();
+    method @NonNull public android.net.Network getPrivateDnsBypassingCopy();
+  }
+
+  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 @Nullable public android.net.Network getNetwork();
+    method public void markConnected();
+    method public void onAddKeepalivePacketFilter(int, @NonNull android.net.KeepalivePacketData);
+    method public void onAutomaticReconnectDisabled();
+    method public void onBandwidthUpdateRequested();
+    method public void onNetworkCreated();
+    method public void onNetworkDestroyed();
+    method public void onNetworkUnwanted();
+    method public void onQosCallbackRegistered(int, @NonNull android.net.QosFilter);
+    method public void onQosCallbackUnregistered(int);
+    method public void onRemoveKeepalivePacketFilter(int);
+    method public void onSaveAcceptUnvalidated(boolean);
+    method public void onSignalStrengthThresholdsUpdated(@NonNull int[]);
+    method public void onStartSocketKeepalive(int, @NonNull java.time.Duration, @NonNull android.net.KeepalivePacketData);
+    method public void onStopSocketKeepalive(int);
+    method public void onValidationStatus(int, @Nullable android.net.Uri);
+    method @NonNull public android.net.Network register();
+    method public final void sendLinkProperties(@NonNull android.net.LinkProperties);
+    method public final void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
+    method public final void sendNetworkScore(@NonNull android.net.NetworkScore);
+    method public final void sendNetworkScore(@IntRange(from=0, to=99) int);
+    method public final void sendQosCallbackError(int, int);
+    method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes);
+    method public final void sendQosSessionLost(int, int, int);
+    method public final void sendSocketKeepaliveEvent(int, int);
+    method @Deprecated public void setLegacySubtype(int, @NonNull String);
+    method public void setLingerDuration(@NonNull java.time.Duration);
+    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();
+    field public static final int VALIDATION_STATUS_NOT_VALID = 2; // 0x2
+    field public static final int VALIDATION_STATUS_VALID = 1; // 0x1
+  }
+
+  public final class NetworkAgentConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getLegacyType();
+    method @NonNull public String getLegacyTypeName();
+    method public boolean isExplicitlySelected();
+    method public boolean isPartialConnectivityAcceptable();
+    method public boolean isUnvalidatedConnectivityAcceptable();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkAgentConfig> CREATOR;
+  }
+
+  public static final class NetworkAgentConfig.Builder {
+    ctor public NetworkAgentConfig.Builder();
+    method @NonNull public android.net.NetworkAgentConfig build();
+    method @NonNull public android.net.NetworkAgentConfig.Builder setExplicitlySelected(boolean);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyExtraInfo(@NonNull String);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacySubType(int);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacySubTypeName(@NonNull String);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyType(int);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyTypeName(@NonNull String);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setNat64DetectionEnabled(boolean);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setPartialConnectivityAcceptable(boolean);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setProvisioningNotificationEnabled(boolean);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setUnvalidatedConnectivityAcceptable(boolean);
+  }
+
+  public final class NetworkCapabilities implements android.os.Parcelable {
+    method @NonNull public int[] getAdministratorUids();
+    method @Nullable public static String getCapabilityCarrierName(int);
+    method @Nullable public String getSsid();
+    method @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds();
+    method @NonNull public int[] getTransportTypes();
+    method public boolean isPrivateDnsBroken();
+    method public boolean satisfiedByNetworkCapabilities(@Nullable android.net.NetworkCapabilities);
+    field public static final int NET_CAPABILITY_BIP = 31; // 0x1f
+    field public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28; // 0x1c
+    field public static final int NET_CAPABILITY_OEM_PAID = 22; // 0x16
+    field public static final int NET_CAPABILITY_OEM_PRIVATE = 26; // 0x1a
+    field public static final int NET_CAPABILITY_PARTIAL_CONNECTIVITY = 24; // 0x18
+    field public static final int NET_CAPABILITY_VEHICLE_INTERNAL = 27; // 0x1b
+    field public static final int NET_CAPABILITY_VSIM = 30; // 0x1e
+  }
+
+  public static final class NetworkCapabilities.Builder {
+    ctor public NetworkCapabilities.Builder();
+    ctor public NetworkCapabilities.Builder(@NonNull android.net.NetworkCapabilities);
+    method @NonNull public android.net.NetworkCapabilities.Builder addCapability(int);
+    method @NonNull public android.net.NetworkCapabilities.Builder addTransportType(int);
+    method @NonNull public android.net.NetworkCapabilities build();
+    method @NonNull public android.net.NetworkCapabilities.Builder removeCapability(int);
+    method @NonNull public android.net.NetworkCapabilities.Builder removeTransportType(int);
+    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAdministratorUids(@NonNull int[]);
+    method @NonNull public android.net.NetworkCapabilities.Builder setLinkDownstreamBandwidthKbps(int);
+    method @NonNull public android.net.NetworkCapabilities.Builder setLinkUpstreamBandwidthKbps(int);
+    method @NonNull public android.net.NetworkCapabilities.Builder setNetworkSpecifier(@Nullable android.net.NetworkSpecifier);
+    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setOwnerUid(int);
+    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setRequestorPackageName(@Nullable String);
+    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setRequestorUid(int);
+    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkCapabilities.Builder setSignalStrength(int);
+    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 static android.net.NetworkCapabilities.Builder withoutDefaultCapabilities();
+  }
+
+  public class NetworkProvider {
+    ctor public NetworkProvider(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String);
+    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void declareNetworkRequestUnfulfillable(@NonNull android.net.NetworkRequest);
+    method public int getProviderId();
+    method public void onNetworkRequestWithdrawn(@NonNull android.net.NetworkRequest);
+    method public void onNetworkRequested(@NonNull android.net.NetworkRequest, @IntRange(from=0, to=99) int, int);
+    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void registerNetworkOffer(@NonNull android.net.NetworkScore, @NonNull android.net.NetworkCapabilities, @NonNull java.util.concurrent.Executor, @NonNull android.net.NetworkProvider.NetworkOfferCallback);
+    method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void unregisterNetworkOffer(@NonNull android.net.NetworkProvider.NetworkOfferCallback);
+    field public static final int ID_NONE = -1; // 0xffffffff
+  }
+
+  public static interface NetworkProvider.NetworkOfferCallback {
+    method public void onNetworkNeeded(@NonNull android.net.NetworkRequest);
+    method public void onNetworkUnneeded(@NonNull android.net.NetworkRequest);
+  }
+
+  public class NetworkReleasedException extends java.lang.Exception {
+  }
+
+  public class NetworkRequest implements android.os.Parcelable {
+    method @Nullable public String getRequestorPackageName();
+    method public int getRequestorUid();
+  }
+
+  public static class NetworkRequest.Builder {
+    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkRequest.Builder setSignalStrength(int);
+    method @NonNull public android.net.NetworkRequest.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
+  }
+
+  public final class NetworkScore implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getKeepConnectedReason();
+    method public int getLegacyInt();
+    method public boolean isExiting();
+    method public boolean isTransportPrimary();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkScore> CREATOR;
+    field public static final int KEEP_CONNECTED_FOR_HANDOVER = 1; // 0x1
+    field public static final int KEEP_CONNECTED_NONE = 0; // 0x0
+  }
+
+  public static final class NetworkScore.Builder {
+    ctor public NetworkScore.Builder();
+    method @NonNull public android.net.NetworkScore build();
+    method @NonNull public android.net.NetworkScore.Builder setExiting(boolean);
+    method @NonNull public android.net.NetworkScore.Builder setKeepConnectedReason(int);
+    method @NonNull public android.net.NetworkScore.Builder setLegacyInt(int);
+    method @NonNull public android.net.NetworkScore.Builder setTransportPrimary(boolean);
+  }
+
+  public final class OemNetworkPreferences implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public java.util.Map<java.lang.String,java.lang.Integer> getNetworkPreferences();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.OemNetworkPreferences> CREATOR;
+    field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID = 1; // 0x1
+    field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK = 2; // 0x2
+    field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY = 3; // 0x3
+    field public static final int OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY = 4; // 0x4
+    field public static final int OEM_NETWORK_PREFERENCE_UNINITIALIZED = 0; // 0x0
+  }
+
+  public static final class OemNetworkPreferences.Builder {
+    ctor public OemNetworkPreferences.Builder();
+    ctor public OemNetworkPreferences.Builder(@NonNull android.net.OemNetworkPreferences);
+    method @NonNull public android.net.OemNetworkPreferences.Builder addNetworkPreference(@NonNull String, int);
+    method @NonNull public android.net.OemNetworkPreferences build();
+    method @NonNull public android.net.OemNetworkPreferences.Builder clearNetworkPreference(@NonNull String);
+  }
+
+  public abstract class QosCallback {
+    ctor public QosCallback();
+    method public void onError(@NonNull android.net.QosCallbackException);
+    method public void onQosSessionAvailable(@NonNull android.net.QosSession, @NonNull android.net.QosSessionAttributes);
+    method public void onQosSessionLost(@NonNull android.net.QosSession);
+  }
+
+  public static class QosCallback.QosCallbackRegistrationException extends java.lang.RuntimeException {
+  }
+
+  public final class QosCallbackException extends java.lang.Exception {
+  }
+
+  public abstract class QosFilter {
+    method @NonNull public abstract android.net.Network getNetwork();
+    method public abstract boolean matchesLocalAddress(@NonNull java.net.InetAddress, int, int);
+    method public abstract boolean matchesRemoteAddress(@NonNull java.net.InetAddress, int, int);
+  }
+
+  public final class QosSession implements android.os.Parcelable {
+    ctor public QosSession(int, int);
+    method public int describeContents();
+    method public int getSessionId();
+    method public int getSessionType();
+    method public long getUniqueId();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.QosSession> CREATOR;
+    field public static final int TYPE_EPS_BEARER = 1; // 0x1
+    field public static final int TYPE_NR_BEARER = 2; // 0x2
+  }
+
+  public interface QosSessionAttributes {
+  }
+
+  public final class QosSocketInfo implements android.os.Parcelable {
+    ctor public QosSocketInfo(@NonNull android.net.Network, @NonNull java.net.Socket) throws java.io.IOException;
+    method public int describeContents();
+    method @NonNull public java.net.InetSocketAddress getLocalSocketAddress();
+    method @NonNull public android.net.Network getNetwork();
+    method @Nullable public java.net.InetSocketAddress getRemoteSocketAddress();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.QosSocketInfo> CREATOR;
+  }
+
+  public final class RouteInfo implements android.os.Parcelable {
+    ctor public RouteInfo(@Nullable android.net.IpPrefix, @Nullable java.net.InetAddress, @Nullable String, int);
+    ctor public RouteInfo(@Nullable android.net.IpPrefix, @Nullable java.net.InetAddress, @Nullable String, int, int);
+    method public int getMtu();
+    method public int getType();
+    field public static final int RTN_THROW = 9; // 0x9
+    field public static final int RTN_UNICAST = 1; // 0x1
+    field public static final int RTN_UNREACHABLE = 7; // 0x7
+  }
+
+  public abstract class SocketKeepalive implements java.lang.AutoCloseable {
+    field public static final int ERROR_NO_SUCH_SLOT = -33; // 0xffffffdf
+    field public static final int SUCCESS = 0; // 0x0
+  }
+
+  public class SocketLocalAddressChangedException extends java.lang.Exception {
+  }
+
+  public class SocketNotBoundException extends java.lang.Exception {
+  }
+
+  public final class StaticIpConfiguration implements android.os.Parcelable {
+    ctor public StaticIpConfiguration();
+    ctor public StaticIpConfiguration(@Nullable android.net.StaticIpConfiguration);
+    method public void addDnsServer(@NonNull java.net.InetAddress);
+    method public void clear();
+    method public int describeContents();
+    method @NonNull public java.util.List<java.net.InetAddress> getDnsServers();
+    method @Nullable public String getDomains();
+    method @Nullable public java.net.InetAddress getGateway();
+    method @Nullable public android.net.LinkAddress getIpAddress();
+    method @NonNull public java.util.List<android.net.RouteInfo> getRoutes(@Nullable String);
+    method public void writeToParcel(android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.StaticIpConfiguration> CREATOR;
+  }
+
+  public static final class StaticIpConfiguration.Builder {
+    ctor public StaticIpConfiguration.Builder();
+    method @NonNull public android.net.StaticIpConfiguration build();
+    method @NonNull public android.net.StaticIpConfiguration.Builder setDnsServers(@NonNull Iterable<java.net.InetAddress>);
+    method @NonNull public android.net.StaticIpConfiguration.Builder setDomains(@Nullable String);
+    method @NonNull public android.net.StaticIpConfiguration.Builder setGateway(@Nullable java.net.InetAddress);
+    method @NonNull public android.net.StaticIpConfiguration.Builder setIpAddress(@Nullable android.net.LinkAddress);
+  }
+
+  public final class TcpKeepalivePacketData extends android.net.KeepalivePacketData implements android.os.Parcelable {
+    ctor public TcpKeepalivePacketData(@NonNull java.net.InetAddress, int, @NonNull java.net.InetAddress, int, @NonNull byte[], int, int, int, int, int, int) throws android.net.InvalidPacketException;
+    method public int describeContents();
+    method public int getIpTos();
+    method public int getIpTtl();
+    method public int getTcpAck();
+    method public int getTcpSeq();
+    method public int getTcpWindow();
+    method public int getTcpWindowScale();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.TcpKeepalivePacketData> CREATOR;
+  }
+
+}
+
+package android.net.apf {
+
+  public final class ApfCapabilities implements android.os.Parcelable {
+    ctor public ApfCapabilities(int, int, int);
+    method public int describeContents();
+    method public static boolean getApfDrop8023Frames();
+    method @NonNull public static int[] getApfEtherTypeBlackList();
+    method public boolean hasDataAccess();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator<android.net.apf.ApfCapabilities> CREATOR;
+    field public final int apfPacketFormat;
+    field public final int apfVersionSupported;
+    field public final int maximumApfProgramSize;
+  }
+
+}
+
diff --git a/framework/api/system-lint-baseline.txt b/framework/api/system-lint-baseline.txt
new file mode 100644
index 0000000..9a97707
--- /dev/null
+++ b/framework/api/system-lint-baseline.txt
@@ -0,0 +1 @@
+// Baseline format: 1.0
diff --git a/framework/api/system-removed.txt b/framework/api/system-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/framework/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/framework/jarjar-rules-proto.txt b/framework/jarjar-rules-proto.txt
new file mode 100644
index 0000000..37b4dec
--- /dev/null
+++ b/framework/jarjar-rules-proto.txt
@@ -0,0 +1,3 @@
+keep android.net.NetworkCapabilitiesProto
+keep android.net.NetworkProto
+keep android.net.NetworkRequestProto
diff --git a/framework/jarjar-rules.txt b/framework/jarjar-rules.txt
new file mode 100644
index 0000000..0959840
--- /dev/null
+++ b/framework/jarjar-rules.txt
@@ -0,0 +1,10 @@
+rule com.android.net.module.util.** android.net.connectivity.framework.util.@1
+
+# TODO (b/149403767): remove the annotations from net-utils-device-common instead of here
+zap android.annotation.**
+zap com.android.net.module.annotation.**
+zap com.android.internal.annotations.**
+
+rule android.net.NetworkCapabilitiesProto* android.net.connectivity.proto.NetworkCapabilitiesProto@1
+rule android.net.NetworkProto* android.net.connectivity.proto.NetworkProto@1
+rule android.net.NetworkRequestProto* android.net.connectivity.proto.NetworkRequestProto@1
diff --git a/framework/jni/android_net_NetworkUtils.cpp b/framework/jni/android_net_NetworkUtils.cpp
new file mode 100644
index 0000000..26484fb
--- /dev/null
+++ b/framework/jni/android_net_NetworkUtils.cpp
@@ -0,0 +1,281 @@
+/*
+ * Copyright 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.
+ */
+
+#define LOG_TAG "NetworkUtils"
+
+#include <vector>
+
+#include <android/file_descriptor_jni.h>
+#include <android/multinetwork.h>
+#include <arpa/inet.h>
+#include <linux/filter.h>
+#include <linux/if_arp.h>
+#include <linux/tcp.h>
+#include <net/if.h>
+#include <netinet/ether.h>
+#include <netinet/ip.h>
+#include <netinet/udp.h>
+
+#include <DnsProxydProtocol.h> // NETID_USE_LOCAL_NAMESERVERS
+#include <cutils/properties.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/JNIPlatformHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+#include <utils/Log.h>
+#include <utils/misc.h>
+
+#include "jni.h"
+
+extern "C" {
+int ifc_enable(const char *ifname);
+int ifc_disable(const char *ifname);
+}
+
+#define NETUTILS_PKG_NAME "android/net/NetworkUtils"
+
+namespace android {
+
+constexpr int MAXPACKETSIZE = 8 * 1024;
+// FrameworkListener limits the size of commands to 4096 bytes.
+constexpr int MAXCMDSIZE = 4096;
+
+static inline jclass FindClassOrDie(JNIEnv* env, const char* class_name) {
+    jclass clazz = env->FindClass(class_name);
+    LOG_ALWAYS_FATAL_IF(clazz == NULL, "Unable to find class %s", class_name);
+    return clazz;
+}
+
+template <typename T>
+static inline T MakeGlobalRefOrDie(JNIEnv* env, T in) {
+    jobject res = env->NewGlobalRef(in);
+    LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to create global reference.");
+    return static_cast<T>(res);
+}
+
+static void android_net_utils_attachDropAllBPFFilter(JNIEnv *env, jobject clazz, jobject javaFd)
+{
+    struct sock_filter filter_code[] = {
+        // Reject all.
+        BPF_STMT(BPF_RET | BPF_K, 0)
+    };
+    struct sock_fprog filter = {
+        sizeof(filter_code) / sizeof(filter_code[0]),
+        filter_code,
+    };
+
+    int fd = AFileDescriptor_getFd(env, javaFd);
+    if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
+    }
+}
+
+static void android_net_utils_detachBPFFilter(JNIEnv *env, jobject clazz, jobject javaFd)
+{
+    int optval_ignored = 0;
+    int fd = AFileDescriptor_getFd(env, javaFd);
+    if (setsockopt(fd, SOL_SOCKET, SO_DETACH_FILTER, &optval_ignored, sizeof(optval_ignored)) !=
+        0) {
+        jniThrowExceptionFmt(env, "java/net/SocketException",
+                "setsockopt(SO_DETACH_FILTER): %s", strerror(errno));
+    }
+}
+
+static jboolean android_net_utils_bindProcessToNetworkHandle(JNIEnv *env, jobject thiz,
+        jlong netHandle)
+{
+    return (jboolean) !android_setprocnetwork(netHandle);
+}
+
+static jlong android_net_utils_getBoundNetworkHandleForProcess(JNIEnv *env, jobject thiz)
+{
+    net_handle_t network;
+    if (android_getprocnetwork(&network) != 0) {
+        jniThrowExceptionFmt(env, "java/lang/IllegalStateException",
+                "android_getprocnetwork(): %s", strerror(errno));
+        return NETWORK_UNSPECIFIED;
+    }
+    return (jlong) network;
+}
+
+static jboolean android_net_utils_bindProcessToNetworkForHostResolution(JNIEnv *env, jobject thiz,
+        jint netId, jlong netHandle)
+{
+    return (jboolean) !android_setprocdns(netHandle);
+}
+
+static jint android_net_utils_bindSocketToNetworkHandle(JNIEnv *env, jobject thiz, jobject javaFd,
+                                                  jlong netHandle) {
+    return android_setsocknetwork(netHandle, AFileDescriptor_getFd(env, javaFd));
+}
+
+static bool checkLenAndCopy(JNIEnv* env, const jbyteArray& addr, int len, void* dst)
+{
+    if (env->GetArrayLength(addr) != len) {
+        return false;
+    }
+    env->GetByteArrayRegion(addr, 0, len, reinterpret_cast<jbyte*>(dst));
+    return true;
+}
+
+static jobject android_net_utils_resNetworkQuery(JNIEnv *env, jobject thiz, jlong netHandle,
+        jstring dname, jint ns_class, jint ns_type, jint flags) {
+    const jsize javaCharsCount = env->GetStringLength(dname);
+    const jsize byteCountUTF8 = env->GetStringUTFLength(dname);
+
+    // Only allow dname which could be simply formatted to UTF8.
+    // In native layer, res_mkquery would re-format the input char array to packet.
+    std::vector<char> queryname(byteCountUTF8 + 1, 0);
+
+    env->GetStringUTFRegion(dname, 0, javaCharsCount, queryname.data());
+
+    int fd = android_res_nquery(netHandle, queryname.data(), ns_class, ns_type, flags);
+
+    if (fd < 0) {
+        jniThrowErrnoException(env, "resNetworkQuery", -fd);
+        return nullptr;
+    }
+
+    return jniCreateFileDescriptor(env, fd);
+}
+
+static jobject android_net_utils_resNetworkSend(JNIEnv *env, jobject thiz, jlong netHandle,
+        jbyteArray msg, jint msgLen, jint flags) {
+    uint8_t data[MAXCMDSIZE];
+
+    checkLenAndCopy(env, msg, msgLen, data);
+    int fd = android_res_nsend(netHandle, data, msgLen, flags);
+
+    if (fd < 0) {
+        jniThrowErrnoException(env, "resNetworkSend", -fd);
+        return nullptr;
+    }
+
+    return jniCreateFileDescriptor(env, fd);
+}
+
+static jobject android_net_utils_resNetworkResult(JNIEnv *env, jobject thiz, jobject javaFd) {
+    int fd = AFileDescriptor_getFd(env, javaFd);
+    int rcode;
+    std::vector<uint8_t> buf(MAXPACKETSIZE, 0);
+
+    int res = android_res_nresult(fd, &rcode, buf.data(), MAXPACKETSIZE);
+    jniSetFileDescriptorOfFD(env, javaFd, -1);
+    if (res < 0) {
+        jniThrowErrnoException(env, "resNetworkResult", -res);
+        return nullptr;
+    }
+
+    jbyteArray answer = env->NewByteArray(res);
+    if (answer == nullptr) {
+        jniThrowErrnoException(env, "resNetworkResult", ENOMEM);
+        return nullptr;
+    } else {
+        env->SetByteArrayRegion(answer, 0, res,
+                reinterpret_cast<jbyte*>(buf.data()));
+    }
+
+    jclass class_DnsResponse = env->FindClass("android/net/DnsResolver$DnsResponse");
+    jmethodID ctor = env->GetMethodID(class_DnsResponse, "<init>", "([BI)V");
+
+    return env->NewObject(class_DnsResponse, ctor, answer, rcode);
+}
+
+static void android_net_utils_resNetworkCancel(JNIEnv *env, jobject thiz, jobject javaFd) {
+    int fd = AFileDescriptor_getFd(env, javaFd);
+    android_res_cancel(fd);
+    jniSetFileDescriptorOfFD(env, javaFd, -1);
+}
+
+static jobject android_net_utils_getDnsNetwork(JNIEnv *env, jobject thiz) {
+    net_handle_t dnsNetHandle = NETWORK_UNSPECIFIED;
+    if (int res = android_getprocdns(&dnsNetHandle) < 0) {
+        jniThrowErrnoException(env, "getDnsNetwork", -res);
+        return nullptr;
+    }
+
+    static jclass class_Network = MakeGlobalRefOrDie(
+            env, FindClassOrDie(env, "android/net/Network"));
+    static jmethodID method = env->GetStaticMethodID(class_Network, "fromNetworkHandle",
+            "(J)Landroid/net/Network;");
+    return env->CallStaticObjectMethod(class_Network, method, static_cast<jlong>(dnsNetHandle));
+}
+
+static jobject android_net_utils_getTcpRepairWindow(JNIEnv *env, jobject thiz, jobject javaFd) {
+    if (javaFd == NULL) {
+        jniThrowNullPointerException(env, NULL);
+        return NULL;
+    }
+
+    int fd = AFileDescriptor_getFd(env, javaFd);
+    struct tcp_repair_window trw = {};
+    socklen_t size = sizeof(trw);
+
+    // Obtain the parameters of the TCP repair window.
+    int rc = getsockopt(fd, IPPROTO_TCP, TCP_REPAIR_WINDOW, &trw, &size);
+    if (rc == -1) {
+        jniThrowErrnoException(env, "getsockopt : TCP_REPAIR_WINDOW", errno);
+        return NULL;
+    }
+
+    struct tcp_info tcpinfo = {};
+    socklen_t tcpinfo_size = sizeof(tcp_info);
+
+    // Obtain the window scale from the tcp info structure. This contains a scale factor that
+    // should be applied to the window size.
+    rc = getsockopt(fd, IPPROTO_TCP, TCP_INFO, &tcpinfo, &tcpinfo_size);
+    if (rc == -1) {
+        jniThrowErrnoException(env, "getsockopt : TCP_INFO", errno);
+        return NULL;
+    }
+
+    jclass class_TcpRepairWindow = env->FindClass("android/net/TcpRepairWindow");
+    jmethodID ctor = env->GetMethodID(class_TcpRepairWindow, "<init>", "(IIIIII)V");
+
+    return env->NewObject(class_TcpRepairWindow, ctor, trw.snd_wl1, trw.snd_wnd, trw.max_window,
+            trw.rcv_wnd, trw.rcv_wup, tcpinfo.tcpi_rcv_wscale);
+}
+
+// ----------------------------------------------------------------------------
+
+/*
+ * JNI registration.
+ */
+// clang-format off
+static const JNINativeMethod gNetworkUtilMethods[] = {
+    /* name, signature, funcPtr */
+    { "bindProcessToNetworkHandle", "(J)Z", (void*) android_net_utils_bindProcessToNetworkHandle },
+    { "getBoundNetworkHandleForProcess", "()J", (void*) android_net_utils_getBoundNetworkHandleForProcess },
+    { "bindProcessToNetworkForHostResolution", "(I)Z", (void*) android_net_utils_bindProcessToNetworkForHostResolution },
+    { "bindSocketToNetworkHandle", "(Ljava/io/FileDescriptor;J)I", (void*) android_net_utils_bindSocketToNetworkHandle },
+    { "attachDropAllBPFFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDropAllBPFFilter },
+    { "detachBPFFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_detachBPFFilter },
+    { "getTcpRepairWindow", "(Ljava/io/FileDescriptor;)Landroid/net/TcpRepairWindow;", (void*) android_net_utils_getTcpRepairWindow },
+    { "resNetworkSend", "(J[BII)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkSend },
+    { "resNetworkQuery", "(JLjava/lang/String;III)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkQuery },
+    { "resNetworkResult", "(Ljava/io/FileDescriptor;)Landroid/net/DnsResolver$DnsResponse;", (void*) android_net_utils_resNetworkResult },
+    { "resNetworkCancel", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_resNetworkCancel },
+    { "getDnsNetwork", "()Landroid/net/Network;", (void*) android_net_utils_getDnsNetwork },
+};
+// clang-format on
+
+int register_android_net_NetworkUtils(JNIEnv* env)
+{
+    return jniRegisterNativeMethods(env, NETUTILS_PKG_NAME, gNetworkUtilMethods,
+                                    NELEM(gNetworkUtilMethods));
+}
+
+}; // namespace android
diff --git a/framework/jni/onload.cpp b/framework/jni/onload.cpp
new file mode 100644
index 0000000..435f434
--- /dev/null
+++ b/framework/jni/onload.cpp
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+#include <nativehelper/JNIHelp.h>
+#include <log/log.h>
+
+namespace android {
+
+int register_android_net_NetworkUtils(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) {
+        ALOGE("GetEnv failed");
+        return JNI_ERR;
+    }
+
+    if (register_android_net_NetworkUtils(env) < 0) {
+        return JNI_ERR;
+    }
+
+    return JNI_VERSION_1_6;
+}
+
+};
\ No newline at end of file
diff --git a/framework/lint-baseline.xml b/framework/lint-baseline.xml
new file mode 100644
index 0000000..099202f
--- /dev/null
+++ b/framework/lint-baseline.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `new android.net.ParseException`"
+        errorLine1="                ParseException pe = new ParseException(e.reason, e.getCause());"
+        errorLine2="                                    ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/DnsResolver.java"
+            line="301"
+            column="37"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback`"
+        errorLine1="    protected class ActiveDataSubscriptionIdListener extends TelephonyCallback"
+        errorLine2="                                                             ~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java"
+            line="96"
+            column="62"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener`"
+        errorLine1="            implements TelephonyCallback.ActiveDataSubscriptionIdListener {"
+        errorLine2="                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java"
+            line="97"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.telephony.TelephonyManager#registerTelephonyCallback`"
+        errorLine1="        ctx.getSystemService(TelephonyManager.class).registerTelephonyCallback("
+        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java"
+            line="126"
+            column="54"/>
+    </issue>
+
+</issues>
diff --git a/framework/src/android/net/CaptivePortal.java b/framework/src/android/net/CaptivePortal.java
new file mode 100644
index 0000000..4a7b601
--- /dev/null
+++ b/framework/src/android/net/CaptivePortal.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed urnder the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.RequiresPermission;
+import android.annotation.SystemApi;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+/**
+ * A class allowing apps handling the {@link ConnectivityManager#ACTION_CAPTIVE_PORTAL_SIGN_IN}
+ * activity to indicate to the system different outcomes of captive portal sign in.  This class is
+ * passed as an extra named {@link ConnectivityManager#EXTRA_CAPTIVE_PORTAL} with the
+ * {@code ACTION_CAPTIVE_PORTAL_SIGN_IN} activity.
+ */
+public class CaptivePortal implements Parcelable {
+    /**
+     * Response code from the captive portal application, indicating that the portal was dismissed
+     * and the network should be re-validated.
+     * @see ICaptivePortal#appResponse(int)
+     * @see android.net.INetworkMonitor#notifyCaptivePortalAppFinished(int)
+     * @hide
+     */
+    @SystemApi
+    public static final int APP_RETURN_DISMISSED    = 0;
+    /**
+     * Response code from the captive portal application, indicating that the user did not login and
+     * does not want to use the captive portal network.
+     * @see ICaptivePortal#appResponse(int)
+     * @see android.net.INetworkMonitor#notifyCaptivePortalAppFinished(int)
+     * @hide
+     */
+    @SystemApi
+    public static final int APP_RETURN_UNWANTED     = 1;
+    /**
+     * Response code from the captive portal application, indicating that the user does not wish to
+     * login but wants to use the captive portal network as-is.
+     * @see ICaptivePortal#appResponse(int)
+     * @see android.net.INetworkMonitor#notifyCaptivePortalAppFinished(int)
+     * @hide
+     */
+    @SystemApi
+    public static final int APP_RETURN_WANTED_AS_IS = 2;
+    /** Event offset of request codes from captive portal application. */
+    private static final int APP_REQUEST_BASE = 100;
+    /**
+     * Request code from the captive portal application, indicating that the network condition may
+     * have changed and the network should be re-validated.
+     * @see ICaptivePortal#appRequest(int)
+     * @see android.net.INetworkMonitor#forceReevaluation(int)
+     * @hide
+     */
+    @SystemApi
+    public static final int APP_REQUEST_REEVALUATION_REQUIRED = APP_REQUEST_BASE + 0;
+
+    private final IBinder mBinder;
+
+    /** @hide */
+    public CaptivePortal(@NonNull IBinder binder) {
+        mBinder = binder;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeStrongBinder(mBinder);
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<CaptivePortal> CREATOR
+            = new Parcelable.Creator<CaptivePortal>() {
+        @Override
+        public CaptivePortal createFromParcel(Parcel in) {
+            return new CaptivePortal(in.readStrongBinder());
+        }
+
+        @Override
+        public CaptivePortal[] newArray(int size) {
+            return new CaptivePortal[size];
+        }
+    };
+
+    /**
+     * Indicate to the system that the captive portal has been
+     * dismissed.  In response the framework will re-evaluate the network's
+     * connectivity and might take further action thereafter.
+     */
+    public void reportCaptivePortalDismissed() {
+        try {
+            ICaptivePortal.Stub.asInterface(mBinder).appResponse(APP_RETURN_DISMISSED);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Indicate to the system that the user does not want to pursue signing in to the
+     * captive portal and the system should continue to prefer other networks
+     * without captive portals for use as the default active data network.  The
+     * system will not retest the network for a captive portal so as to avoid
+     * disturbing the user with further sign in to network notifications.
+     */
+    public void ignoreNetwork() {
+        try {
+            ICaptivePortal.Stub.asInterface(mBinder).appResponse(APP_RETURN_UNWANTED);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Indicate to the system the user wants to use this network as is, even though
+     * the captive portal is still in place.  The system will treat the network
+     * as if it did not have a captive portal when selecting the network to use
+     * as the default active data network. This may result in this network
+     * becoming the default active data network, which could disrupt network
+     * connectivity for apps because the captive portal is still in place.
+     * @hide
+     */
+    @SystemApi
+    public void useNetwork() {
+        try {
+            ICaptivePortal.Stub.asInterface(mBinder).appResponse(APP_RETURN_WANTED_AS_IS);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Request that the system reevaluates the captive portal status.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+    public void reevaluateNetwork() {
+        try {
+            ICaptivePortal.Stub.asInterface(mBinder).appRequest(APP_REQUEST_REEVALUATION_REQUIRED);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Log a captive portal login event.
+     * @param eventId one of the CAPTIVE_PORTAL_LOGIN_* constants in metrics_constants.proto.
+     * @param packageName captive portal application package name.
+     * @hide
+     * @deprecated The event will not be logged in Android S and above. The
+     * caller is migrating to statsd.
+     */
+    @Deprecated
+    @SystemApi
+    public void logEvent(int eventId, @NonNull String packageName) {
+    }
+}
diff --git a/framework/src/android/net/CaptivePortalData.java b/framework/src/android/net/CaptivePortalData.java
new file mode 100644
index 0000000..53aa1b9
--- /dev/null
+++ b/framework/src/android/net/CaptivePortalData.java
@@ -0,0 +1,379 @@
+/*
+ * 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;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Metadata sent by captive portals, see https://www.ietf.org/id/draft-ietf-capport-api-03.txt.
+ * @hide
+ */
+@SystemApi
+public final class CaptivePortalData implements Parcelable {
+    private final long mRefreshTimeMillis;
+    @Nullable
+    private final Uri mUserPortalUrl;
+    @Nullable
+    private final Uri mVenueInfoUrl;
+    private final boolean mIsSessionExtendable;
+    private final long mByteLimit;
+    private final long mExpiryTimeMillis;
+    private final boolean mCaptive;
+    private final String mVenueFriendlyName;
+    private final int mVenueInfoUrlSource;
+    private final int mUserPortalUrlSource;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"CAPTIVE_PORTAL_DATA_SOURCE_"}, value = {
+            CAPTIVE_PORTAL_DATA_SOURCE_OTHER,
+            CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT})
+    public @interface CaptivePortalDataSource {}
+
+    /**
+     * Source of information: Other (default)
+     */
+    public static final int CAPTIVE_PORTAL_DATA_SOURCE_OTHER = 0;
+
+    /**
+     * Source of information: Wi-Fi Passpoint
+     */
+    public static final int CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT = 1;
+
+    private CaptivePortalData(long refreshTimeMillis, Uri userPortalUrl, Uri venueInfoUrl,
+            boolean isSessionExtendable, long byteLimit, long expiryTimeMillis, boolean captive,
+            CharSequence venueFriendlyName, int venueInfoUrlSource, int userPortalUrlSource) {
+        mRefreshTimeMillis = refreshTimeMillis;
+        mUserPortalUrl = userPortalUrl;
+        mVenueInfoUrl = venueInfoUrl;
+        mIsSessionExtendable = isSessionExtendable;
+        mByteLimit = byteLimit;
+        mExpiryTimeMillis = expiryTimeMillis;
+        mCaptive = captive;
+        mVenueFriendlyName = venueFriendlyName == null ? null : venueFriendlyName.toString();
+        mVenueInfoUrlSource = venueInfoUrlSource;
+        mUserPortalUrlSource = userPortalUrlSource;
+    }
+
+    private CaptivePortalData(Parcel p) {
+        this(p.readLong(), p.readParcelable(null), p.readParcelable(null), p.readBoolean(),
+                p.readLong(), p.readLong(), p.readBoolean(), p.readString(), p.readInt(),
+                p.readInt());
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeLong(mRefreshTimeMillis);
+        dest.writeParcelable(mUserPortalUrl, 0);
+        dest.writeParcelable(mVenueInfoUrl, 0);
+        dest.writeBoolean(mIsSessionExtendable);
+        dest.writeLong(mByteLimit);
+        dest.writeLong(mExpiryTimeMillis);
+        dest.writeBoolean(mCaptive);
+        dest.writeString(mVenueFriendlyName);
+        dest.writeInt(mVenueInfoUrlSource);
+        dest.writeInt(mUserPortalUrlSource);
+    }
+
+    /**
+     * A builder to create new {@link CaptivePortalData}.
+     */
+    public static class Builder {
+        private long mRefreshTime;
+        private Uri mUserPortalUrl;
+        private Uri mVenueInfoUrl;
+        private boolean mIsSessionExtendable;
+        private long mBytesRemaining = -1;
+        private long mExpiryTime = -1;
+        private boolean mCaptive;
+        private CharSequence mVenueFriendlyName;
+        private @CaptivePortalDataSource int mVenueInfoUrlSource = CAPTIVE_PORTAL_DATA_SOURCE_OTHER;
+        private @CaptivePortalDataSource int mUserPortalUrlSource =
+                CAPTIVE_PORTAL_DATA_SOURCE_OTHER;
+
+        /**
+         * Create an empty builder.
+         */
+        public Builder() {}
+
+        /**
+         * Create a builder copying all data from existing {@link CaptivePortalData}.
+         */
+        public Builder(@Nullable CaptivePortalData data) {
+            if (data == null) return;
+            setRefreshTime(data.mRefreshTimeMillis)
+                    .setUserPortalUrl(data.mUserPortalUrl, data.mUserPortalUrlSource)
+                    .setVenueInfoUrl(data.mVenueInfoUrl, data.mVenueInfoUrlSource)
+                    .setSessionExtendable(data.mIsSessionExtendable)
+                    .setBytesRemaining(data.mByteLimit)
+                    .setExpiryTime(data.mExpiryTimeMillis)
+                    .setCaptive(data.mCaptive)
+                    .setVenueFriendlyName(data.mVenueFriendlyName);
+        }
+
+        /**
+         * Set the time at which data was last refreshed, as per {@link System#currentTimeMillis()}.
+         */
+        @NonNull
+        public Builder setRefreshTime(long refreshTime) {
+            mRefreshTime = refreshTime;
+            return this;
+        }
+
+        /**
+         * Set the URL to be used for users to login to the portal, if captive.
+         */
+        @NonNull
+        public Builder setUserPortalUrl(@Nullable Uri userPortalUrl) {
+            return setUserPortalUrl(userPortalUrl, CAPTIVE_PORTAL_DATA_SOURCE_OTHER);
+        }
+
+        /**
+         * Set the URL to be used for users to login to the portal, if captive, and the source of
+         * the data, see {@link CaptivePortalDataSource}
+         */
+        @NonNull
+        public Builder setUserPortalUrl(@Nullable Uri userPortalUrl,
+                @CaptivePortalDataSource int source) {
+            mUserPortalUrl = userPortalUrl;
+            mUserPortalUrlSource = source;
+            return this;
+        }
+
+        /**
+         * Set the URL that can be used by users to view information about the network venue.
+         */
+        @NonNull
+        public Builder setVenueInfoUrl(@Nullable Uri venueInfoUrl) {
+            return setVenueInfoUrl(venueInfoUrl, CAPTIVE_PORTAL_DATA_SOURCE_OTHER);
+        }
+
+        /**
+         * Set the URL that can be used by users to view information about the network venue, and
+         * the source of the data, see {@link CaptivePortalDataSource}
+         */
+        @NonNull
+        public Builder setVenueInfoUrl(@Nullable Uri venueInfoUrl,
+                @CaptivePortalDataSource int source) {
+            mVenueInfoUrl = venueInfoUrl;
+            mVenueInfoUrlSource = source;
+            return this;
+        }
+
+        /**
+         * Set whether the portal supports extending a user session on the portal URL page.
+         */
+        @NonNull
+        public Builder setSessionExtendable(boolean sessionExtendable) {
+            mIsSessionExtendable = sessionExtendable;
+            return this;
+        }
+
+        /**
+         * Set the number of bytes remaining on the network before the portal closes.
+         */
+        @NonNull
+        public Builder setBytesRemaining(long bytesRemaining) {
+            mBytesRemaining = bytesRemaining;
+            return this;
+        }
+
+        /**
+         * Set the time at the session will expire, as per {@link System#currentTimeMillis()}.
+         */
+        @NonNull
+        public Builder setExpiryTime(long expiryTime) {
+            mExpiryTime = expiryTime;
+            return this;
+        }
+
+        /**
+         * Set whether the network is captive (portal closed).
+         */
+        @NonNull
+        public Builder setCaptive(boolean captive) {
+            mCaptive = captive;
+            return this;
+        }
+
+        /**
+         * Set the venue friendly name.
+         */
+        @NonNull
+        public Builder setVenueFriendlyName(@Nullable CharSequence venueFriendlyName) {
+            mVenueFriendlyName = venueFriendlyName;
+            return this;
+        }
+
+        /**
+         * Create a new {@link CaptivePortalData}.
+         */
+        @NonNull
+        public CaptivePortalData build() {
+            return new CaptivePortalData(mRefreshTime, mUserPortalUrl, mVenueInfoUrl,
+                    mIsSessionExtendable, mBytesRemaining, mExpiryTime, mCaptive,
+                    mVenueFriendlyName, mVenueInfoUrlSource,
+                    mUserPortalUrlSource);
+        }
+    }
+
+    /**
+     * Get the time at which data was last refreshed, as per {@link System#currentTimeMillis()}.
+     */
+    public long getRefreshTimeMillis() {
+        return mRefreshTimeMillis;
+    }
+
+    /**
+     * Get the URL to be used for users to login to the portal, or extend their session if
+     * {@link #isSessionExtendable()} is true.
+     */
+    @Nullable
+    public Uri getUserPortalUrl() {
+        return mUserPortalUrl;
+    }
+
+    /**
+     * Get the URL that can be used by users to view information about the network venue.
+     */
+    @Nullable
+    public Uri getVenueInfoUrl() {
+        return mVenueInfoUrl;
+    }
+
+    /**
+     * Indicates whether the user portal URL can be used to extend sessions, when the user is logged
+     * in and the session has a time or byte limit.
+     */
+    public boolean isSessionExtendable() {
+        return mIsSessionExtendable;
+    }
+
+    /**
+     * Get the remaining bytes on the captive portal session, at the time {@link CaptivePortalData}
+     * was refreshed. This may be different from the limit currently enforced by the portal.
+     * @return The byte limit, or -1 if not set.
+     */
+    public long getByteLimit() {
+        return mByteLimit;
+    }
+
+    /**
+     * Get the time at the session will expire, as per {@link System#currentTimeMillis()}.
+     * @return The expiry time, or -1 if unset.
+     */
+    public long getExpiryTimeMillis() {
+        return mExpiryTimeMillis;
+    }
+
+    /**
+     * Get whether the network is captive (portal closed).
+     */
+    public boolean isCaptive() {
+        return mCaptive;
+    }
+
+    /**
+     * Get the information source of the Venue URL
+     * @return The source that the Venue URL was obtained from
+     */
+    public @CaptivePortalDataSource int getVenueInfoUrlSource() {
+        return mVenueInfoUrlSource;
+    }
+
+    /**
+     * Get the information source of the user portal URL
+     * @return The source that the user portal URL was obtained from
+     */
+    public @CaptivePortalDataSource int getUserPortalUrlSource() {
+        return mUserPortalUrlSource;
+    }
+
+    /**
+     * Get the venue friendly name
+     */
+    @Nullable
+    public CharSequence getVenueFriendlyName() {
+        return mVenueFriendlyName;
+    }
+
+    @NonNull
+    public static final Creator<CaptivePortalData> CREATOR = new Creator<CaptivePortalData>() {
+        @Override
+        public CaptivePortalData createFromParcel(Parcel source) {
+            return new CaptivePortalData(source);
+        }
+
+        @Override
+        public CaptivePortalData[] newArray(int size) {
+            return new CaptivePortalData[size];
+        }
+    };
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mRefreshTimeMillis, mUserPortalUrl, mVenueInfoUrl,
+                mIsSessionExtendable, mByteLimit, mExpiryTimeMillis, mCaptive, mVenueFriendlyName,
+                mVenueInfoUrlSource, mUserPortalUrlSource);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof CaptivePortalData)) return false;
+        final CaptivePortalData other = (CaptivePortalData) obj;
+        return mRefreshTimeMillis == other.mRefreshTimeMillis
+                && Objects.equals(mUserPortalUrl, other.mUserPortalUrl)
+                && Objects.equals(mVenueInfoUrl, other.mVenueInfoUrl)
+                && mIsSessionExtendable == other.mIsSessionExtendable
+                && mByteLimit == other.mByteLimit
+                && mExpiryTimeMillis == other.mExpiryTimeMillis
+                && mCaptive == other.mCaptive
+                && Objects.equals(mVenueFriendlyName, other.mVenueFriendlyName)
+                && mVenueInfoUrlSource == other.mVenueInfoUrlSource
+                && mUserPortalUrlSource == other.mUserPortalUrlSource;
+    }
+
+    @Override
+    public String toString() {
+        return "CaptivePortalData {"
+                + "refreshTime: " + mRefreshTimeMillis
+                + ", userPortalUrl: " + mUserPortalUrl
+                + ", venueInfoUrl: " + mVenueInfoUrl
+                + ", isSessionExtendable: " + mIsSessionExtendable
+                + ", byteLimit: " + mByteLimit
+                + ", expiryTime: " + mExpiryTimeMillis
+                + ", captive: " + mCaptive
+                + ", venueFriendlyName: " + mVenueFriendlyName
+                + ", venueInfoUrlSource: " + mVenueInfoUrlSource
+                + ", userPortalUrlSource: " + mUserPortalUrlSource
+                + "}";
+    }
+}
diff --git a/framework/src/android/net/ConnectionInfo.aidl b/framework/src/android/net/ConnectionInfo.aidl
new file mode 100644
index 0000000..07faf8b
--- /dev/null
+++ b/framework/src/android/net/ConnectionInfo.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;
+
+parcelable ConnectionInfo;
diff --git a/framework/src/android/net/ConnectionInfo.java b/framework/src/android/net/ConnectionInfo.java
new file mode 100644
index 0000000..4514a84
--- /dev/null
+++ b/framework/src/android/net/ConnectionInfo.java
@@ -0,0 +1,83 @@
+/*
+ * 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;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Describe a network connection including local and remote address/port of a connection and the
+ * transport protocol.
+ *
+ * @hide
+ */
+public final class ConnectionInfo implements Parcelable {
+    public final int protocol;
+    public final InetSocketAddress local;
+    public final InetSocketAddress remote;
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public ConnectionInfo(int protocol, InetSocketAddress local, InetSocketAddress remote) {
+        this.protocol = protocol;
+        this.local = local;
+        this.remote = remote;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(protocol);
+        out.writeByteArray(local.getAddress().getAddress());
+        out.writeInt(local.getPort());
+        out.writeByteArray(remote.getAddress().getAddress());
+        out.writeInt(remote.getPort());
+    }
+
+    public static final @android.annotation.NonNull Creator<ConnectionInfo> CREATOR = new Creator<ConnectionInfo>() {
+        public ConnectionInfo createFromParcel(Parcel in) {
+            int protocol = in.readInt();
+            InetAddress localAddress;
+            try {
+                localAddress = InetAddress.getByAddress(in.createByteArray());
+            } catch (UnknownHostException e) {
+                throw new IllegalArgumentException("Invalid InetAddress");
+            }
+            int localPort = in.readInt();
+            InetAddress remoteAddress;
+            try {
+                remoteAddress = InetAddress.getByAddress(in.createByteArray());
+            } catch (UnknownHostException e) {
+                throw new IllegalArgumentException("Invalid InetAddress");
+            }
+            int remotePort = in.readInt();
+            InetSocketAddress local = new InetSocketAddress(localAddress, localPort);
+            InetSocketAddress remote = new InetSocketAddress(remoteAddress, remotePort);
+            return new ConnectionInfo(protocol, local, remote);
+        }
+
+        public ConnectionInfo[] newArray(int size) {
+            return new ConnectionInfo[size];
+        }
+    };
+}
diff --git a/framework/src/android/net/ConnectivityDiagnosticsManager.java b/framework/src/android/net/ConnectivityDiagnosticsManager.java
new file mode 100644
index 0000000..dcc8a5e
--- /dev/null
+++ b/framework/src/android/net/ConnectivityDiagnosticsManager.java
@@ -0,0 +1,778 @@
+/*
+ * 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;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * Class that provides utilities for collecting network connectivity diagnostics information.
+ * Connectivity information is made available through triggerable diagnostics tools and by listening
+ * to System validations. Some diagnostics information may be permissions-restricted.
+ *
+ * <p>ConnectivityDiagnosticsManager is intended for use by applications offering network
+ * connectivity on a user device. These tools will provide several mechanisms for these applications
+ * to be alerted to network conditions as well as diagnose potential network issues themselves.
+ *
+ * <p>The primary responsibilities of this class are to:
+ *
+ * <ul>
+ *   <li>Allow permissioned applications to register and unregister callbacks for network event
+ *       notifications
+ *   <li>Invoke callbacks for network event notifications, including:
+ *       <ul>
+ *         <li>Network validations
+ *         <li>Data stalls
+ *         <li>Connectivity reports from applications
+ *       </ul>
+ * </ul>
+ */
+public class ConnectivityDiagnosticsManager {
+    /** @hide */
+    @VisibleForTesting
+    public static final Map<ConnectivityDiagnosticsCallback, ConnectivityDiagnosticsBinder>
+            sCallbacks = new ConcurrentHashMap<>();
+
+    private final Context mContext;
+    private final IConnectivityManager mService;
+
+    /** @hide */
+    public ConnectivityDiagnosticsManager(Context context, IConnectivityManager service) {
+        mContext = Objects.requireNonNull(context, "missing context");
+        mService = Objects.requireNonNull(service, "missing IConnectivityManager");
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public static boolean persistableBundleEquals(
+            @Nullable PersistableBundle a, @Nullable PersistableBundle b) {
+        if (a == b) return true;
+        if (a == null || b == null) return false;
+        if (!Objects.equals(a.keySet(), b.keySet())) return false;
+        for (String key : a.keySet()) {
+            if (!Objects.equals(a.get(key), b.get(key))) return false;
+        }
+        return true;
+    }
+
+    /** Class that includes connectivity information for a specific Network at a specific time. */
+    public static final class ConnectivityReport implements Parcelable {
+        /**
+         * The overall status of the network is that it is invalid; it neither provides
+         * connectivity nor has been exempted from validation.
+         */
+        public static final int NETWORK_VALIDATION_RESULT_INVALID = 0;
+
+        /**
+         * The overall status of the network is that it is valid, this may be because it provides
+         * full Internet access (all probes succeeded), or because other properties of the network
+         * caused probes not to be run.
+         */
+        // TODO: link to INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID
+        public static final int NETWORK_VALIDATION_RESULT_VALID = 1;
+
+        /**
+         * The overall status of the network is that it provides partial connectivity; some
+         * probed services succeeded but others failed.
+         */
+        // TODO: link to INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
+        public static final int NETWORK_VALIDATION_RESULT_PARTIALLY_VALID = 2;
+
+        /**
+         * Due to the properties of the network, validation was not performed.
+         */
+        public static final int NETWORK_VALIDATION_RESULT_SKIPPED = 3;
+
+        /** @hide */
+        @IntDef(
+                prefix = {"NETWORK_VALIDATION_RESULT_"},
+                value = {
+                        NETWORK_VALIDATION_RESULT_INVALID,
+                        NETWORK_VALIDATION_RESULT_VALID,
+                        NETWORK_VALIDATION_RESULT_PARTIALLY_VALID,
+                        NETWORK_VALIDATION_RESULT_SKIPPED
+                })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface NetworkValidationResult {}
+
+        /**
+         * The overall validation result for the Network being reported on.
+         *
+         * <p>The possible values for this key are:
+         * {@link #NETWORK_VALIDATION_RESULT_INVALID},
+         * {@link #NETWORK_VALIDATION_RESULT_VALID},
+         * {@link #NETWORK_VALIDATION_RESULT_PARTIALLY_VALID},
+         * {@link #NETWORK_VALIDATION_RESULT_SKIPPED}.
+         *
+         * @see android.net.NetworkCapabilities#NET_CAPABILITY_VALIDATED
+         */
+        @NetworkValidationResult
+        public static final String KEY_NETWORK_VALIDATION_RESULT = "networkValidationResult";
+
+        /** DNS probe. */
+        // TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
+        public static final int NETWORK_PROBE_DNS = 0x04;
+
+        /** HTTP probe. */
+        // TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP
+        public static final int NETWORK_PROBE_HTTP = 0x08;
+
+        /** HTTPS probe. */
+        // TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS;
+        public static final int NETWORK_PROBE_HTTPS = 0x10;
+
+        /** Captive portal fallback probe. */
+        // TODO: link to INetworkMonitor.NETWORK_VALIDATION_FALLBACK
+        public static final int NETWORK_PROBE_FALLBACK = 0x20;
+
+        /** Private DNS (DNS over TLS) probd. */
+        // TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS
+        public static final int NETWORK_PROBE_PRIVATE_DNS = 0x40;
+
+        /** @hide */
+        @IntDef(
+                prefix = {"NETWORK_PROBE_"},
+                value = {
+                        NETWORK_PROBE_DNS,
+                        NETWORK_PROBE_HTTP,
+                        NETWORK_PROBE_HTTPS,
+                        NETWORK_PROBE_FALLBACK,
+                        NETWORK_PROBE_PRIVATE_DNS
+                })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface NetworkProbe {}
+
+        /**
+         * A bitmask of network validation probes that succeeded.
+         *
+         * <p>The possible bits values reported by this key are:
+         * {@link #NETWORK_PROBE_DNS},
+         * {@link #NETWORK_PROBE_HTTP},
+         * {@link #NETWORK_PROBE_HTTPS},
+         * {@link #NETWORK_PROBE_FALLBACK},
+         * {@link #NETWORK_PROBE_PRIVATE_DNS}.
+         */
+        @NetworkProbe
+        public static final String KEY_NETWORK_PROBES_SUCCEEDED_BITMASK =
+                "networkProbesSucceeded";
+
+        /**
+         * A bitmask of network validation probes that were attempted.
+         *
+         * <p>These probes may have failed or may be incomplete at the time of this report.
+         *
+         * <p>The possible bits values reported by this key are:
+         * {@link #NETWORK_PROBE_DNS},
+         * {@link #NETWORK_PROBE_HTTP},
+         * {@link #NETWORK_PROBE_HTTPS},
+         * {@link #NETWORK_PROBE_FALLBACK},
+         * {@link #NETWORK_PROBE_PRIVATE_DNS}.
+         */
+        @NetworkProbe
+        public static final String KEY_NETWORK_PROBES_ATTEMPTED_BITMASK =
+                "networkProbesAttempted";
+
+        /** @hide */
+        @StringDef(prefix = {"KEY_"}, value = {
+                KEY_NETWORK_VALIDATION_RESULT, KEY_NETWORK_PROBES_SUCCEEDED_BITMASK,
+                KEY_NETWORK_PROBES_ATTEMPTED_BITMASK})
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface ConnectivityReportBundleKeys {}
+
+        /** The Network for which this ConnectivityReport applied */
+        @NonNull private final Network mNetwork;
+
+        /**
+         * The timestamp for the report. The timestamp is taken from {@link
+         * System#currentTimeMillis}.
+         */
+        private final long mReportTimestamp;
+
+        /** LinkProperties available on the Network at the reported timestamp */
+        @NonNull private final LinkProperties mLinkProperties;
+
+        /** NetworkCapabilities available on the Network at the reported timestamp */
+        @NonNull private final NetworkCapabilities mNetworkCapabilities;
+
+        /** PersistableBundle that may contain additional info about the report */
+        @NonNull private final PersistableBundle mAdditionalInfo;
+
+        /**
+         * Constructor for ConnectivityReport.
+         *
+         * <p>Apps should obtain instances through {@link
+         * ConnectivityDiagnosticsCallback#onConnectivityReportAvailable} instead of instantiating
+         * their own instances (unless for testing purposes).
+         *
+         * @param network The Network for which this ConnectivityReport applies
+         * @param reportTimestamp The timestamp for the report
+         * @param linkProperties The LinkProperties available on network at reportTimestamp
+         * @param networkCapabilities The NetworkCapabilities available on network at
+         *     reportTimestamp
+         * @param additionalInfo A PersistableBundle that may contain additional info about the
+         *     report
+         */
+        public ConnectivityReport(
+                @NonNull Network network,
+                long reportTimestamp,
+                @NonNull LinkProperties linkProperties,
+                @NonNull NetworkCapabilities networkCapabilities,
+                @NonNull PersistableBundle additionalInfo) {
+            mNetwork = network;
+            mReportTimestamp = reportTimestamp;
+            mLinkProperties = new LinkProperties(linkProperties);
+            mNetworkCapabilities = new NetworkCapabilities(networkCapabilities);
+            mAdditionalInfo = additionalInfo;
+        }
+
+        /**
+         * Returns the Network for this ConnectivityReport.
+         *
+         * @return The Network for which this ConnectivityReport applied
+         */
+        @NonNull
+        public Network getNetwork() {
+            return mNetwork;
+        }
+
+        /**
+         * Returns the epoch timestamp (milliseconds) for when this report was taken.
+         *
+         * @return The timestamp for the report. Taken from {@link System#currentTimeMillis}.
+         */
+        public long getReportTimestamp() {
+            return mReportTimestamp;
+        }
+
+        /**
+         * Returns the LinkProperties available when this report was taken.
+         *
+         * @return LinkProperties available on the Network at the reported timestamp
+         */
+        @NonNull
+        public LinkProperties getLinkProperties() {
+            return new LinkProperties(mLinkProperties);
+        }
+
+        /**
+         * Returns the NetworkCapabilities when this report was taken.
+         *
+         * @return NetworkCapabilities available on the Network at the reported timestamp
+         */
+        @NonNull
+        public NetworkCapabilities getNetworkCapabilities() {
+            return new NetworkCapabilities(mNetworkCapabilities);
+        }
+
+        /**
+         * Returns a PersistableBundle with additional info for this report.
+         *
+         * @return PersistableBundle that may contain additional info about the report
+         */
+        @NonNull
+        public PersistableBundle getAdditionalInfo() {
+            return new PersistableBundle(mAdditionalInfo);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object o) {
+            if (this == o) return true;
+            if (!(o instanceof ConnectivityReport)) return false;
+            final ConnectivityReport that = (ConnectivityReport) o;
+
+            // PersistableBundle is optimized to avoid unparcelling data unless fields are
+            // referenced. Because of this, use {@link ConnectivityDiagnosticsManager#equals} over
+            // {@link PersistableBundle#kindofEquals}.
+            return mReportTimestamp == that.mReportTimestamp
+                    && mNetwork.equals(that.mNetwork)
+                    && mLinkProperties.equals(that.mLinkProperties)
+                    && mNetworkCapabilities.equals(that.mNetworkCapabilities)
+                    && persistableBundleEquals(mAdditionalInfo, that.mAdditionalInfo);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(
+                    mNetwork,
+                    mReportTimestamp,
+                    mLinkProperties,
+                    mNetworkCapabilities,
+                    mAdditionalInfo);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            dest.writeParcelable(mNetwork, flags);
+            dest.writeLong(mReportTimestamp);
+            dest.writeParcelable(mLinkProperties, flags);
+            dest.writeParcelable(mNetworkCapabilities, flags);
+            dest.writeParcelable(mAdditionalInfo, flags);
+        }
+
+        /** Implement the Parcelable interface */
+        public static final @NonNull Creator<ConnectivityReport> CREATOR =
+                new Creator<ConnectivityReport>() {
+                    public ConnectivityReport createFromParcel(Parcel in) {
+                        return new ConnectivityReport(
+                                in.readParcelable(null),
+                                in.readLong(),
+                                in.readParcelable(null),
+                                in.readParcelable(null),
+                                in.readParcelable(null));
+                    }
+
+                    public ConnectivityReport[] newArray(int size) {
+                        return new ConnectivityReport[size];
+                    }
+                };
+    }
+
+    /** Class that includes information for a suspected data stall on a specific Network */
+    public static final class DataStallReport implements Parcelable {
+        /**
+         * Indicates that the Data Stall was detected using DNS events.
+         */
+        public static final int DETECTION_METHOD_DNS_EVENTS = 1;
+
+        /**
+         * Indicates that the Data Stall was detected using TCP metrics.
+         */
+        public static final int DETECTION_METHOD_TCP_METRICS = 2;
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(
+                prefix = {"DETECTION_METHOD_"},
+                value = {DETECTION_METHOD_DNS_EVENTS, DETECTION_METHOD_TCP_METRICS})
+        public @interface DetectionMethod {}
+
+        /**
+         * This key represents the period in milliseconds over which other included TCP metrics
+         * were measured.
+         *
+         * <p>This key will be included if the data stall detection method is
+         * {@link #DETECTION_METHOD_TCP_METRICS}.
+         *
+         * <p>This value is an int.
+         */
+        public static final String KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS =
+                "tcpMetricsCollectionPeriodMillis";
+
+        /**
+         * This key represents the fail rate of TCP packets when the suspected data stall was
+         * detected.
+         *
+         * <p>This key will be included if the data stall detection method is
+         * {@link #DETECTION_METHOD_TCP_METRICS}.
+         *
+         * <p>This value is an int percentage between 0 and 100.
+         */
+        public static final String KEY_TCP_PACKET_FAIL_RATE = "tcpPacketFailRate";
+
+        /**
+         * This key represents the consecutive number of DNS timeouts that have occurred.
+         *
+         * <p>The consecutive count will be reset any time a DNS response is received.
+         *
+         * <p>This key will be included if the data stall detection method is
+         * {@link #DETECTION_METHOD_DNS_EVENTS}.
+         *
+         * <p>This value is an int.
+         */
+        public static final String KEY_DNS_CONSECUTIVE_TIMEOUTS = "dnsConsecutiveTimeouts";
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @StringDef(prefix = {"KEY_"}, value = {
+                KEY_TCP_PACKET_FAIL_RATE,
+                KEY_DNS_CONSECUTIVE_TIMEOUTS
+        })
+        public @interface DataStallReportBundleKeys {}
+
+        /** The Network for which this DataStallReport applied */
+        @NonNull private final Network mNetwork;
+
+        /**
+         * The timestamp for the report. The timestamp is taken from {@link
+         * System#currentTimeMillis}.
+         */
+        private long mReportTimestamp;
+
+        /** A bitmask of the detection methods used to identify the suspected data stall */
+        @DetectionMethod private final int mDetectionMethod;
+
+        /** LinkProperties available on the Network at the reported timestamp */
+        @NonNull private final LinkProperties mLinkProperties;
+
+        /** NetworkCapabilities available on the Network at the reported timestamp */
+        @NonNull private final NetworkCapabilities mNetworkCapabilities;
+
+        /** PersistableBundle that may contain additional information on the suspected data stall */
+        @NonNull private final PersistableBundle mStallDetails;
+
+        /**
+         * Constructor for DataStallReport.
+         *
+         * <p>Apps should obtain instances through {@link
+         * ConnectivityDiagnosticsCallback#onDataStallSuspected} instead of instantiating their own
+         * instances (unless for testing purposes).
+         *
+         * @param network The Network for which this DataStallReport applies
+         * @param reportTimestamp The timestamp for the report
+         * @param detectionMethod The detection method used to identify this data stall
+         * @param linkProperties The LinkProperties available on network at reportTimestamp
+         * @param networkCapabilities The NetworkCapabilities available on network at
+         *     reportTimestamp
+         * @param stallDetails A PersistableBundle that may contain additional info about the report
+         */
+        public DataStallReport(
+                @NonNull Network network,
+                long reportTimestamp,
+                @DetectionMethod int detectionMethod,
+                @NonNull LinkProperties linkProperties,
+                @NonNull NetworkCapabilities networkCapabilities,
+                @NonNull PersistableBundle stallDetails) {
+            mNetwork = network;
+            mReportTimestamp = reportTimestamp;
+            mDetectionMethod = detectionMethod;
+            mLinkProperties = new LinkProperties(linkProperties);
+            mNetworkCapabilities = new NetworkCapabilities(networkCapabilities);
+            mStallDetails = stallDetails;
+        }
+
+        /**
+         * Returns the Network for this DataStallReport.
+         *
+         * @return The Network for which this DataStallReport applied
+         */
+        @NonNull
+        public Network getNetwork() {
+            return mNetwork;
+        }
+
+        /**
+         * Returns the epoch timestamp (milliseconds) for when this report was taken.
+         *
+         * @return The timestamp for the report. Taken from {@link System#currentTimeMillis}.
+         */
+        public long getReportTimestamp() {
+            return mReportTimestamp;
+        }
+
+        /**
+         * Returns the bitmask of detection methods used to identify this suspected data stall.
+         *
+         * @return The bitmask of detection methods used to identify the suspected data stall
+         */
+        public int getDetectionMethod() {
+            return mDetectionMethod;
+        }
+
+        /**
+         * Returns the LinkProperties available when this report was taken.
+         *
+         * @return LinkProperties available on the Network at the reported timestamp
+         */
+        @NonNull
+        public LinkProperties getLinkProperties() {
+            return new LinkProperties(mLinkProperties);
+        }
+
+        /**
+         * Returns the NetworkCapabilities when this report was taken.
+         *
+         * @return NetworkCapabilities available on the Network at the reported timestamp
+         */
+        @NonNull
+        public NetworkCapabilities getNetworkCapabilities() {
+            return new NetworkCapabilities(mNetworkCapabilities);
+        }
+
+        /**
+         * Returns a PersistableBundle with additional info for this report.
+         *
+         * <p>Gets a bundle with details about the suspected data stall including information
+         * specific to the monitoring method that detected the data stall.
+         *
+         * @return PersistableBundle that may contain additional information on the suspected data
+         *     stall
+         */
+        @NonNull
+        public PersistableBundle getStallDetails() {
+            return new PersistableBundle(mStallDetails);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object o) {
+            if (this == o) return true;
+            if (!(o instanceof DataStallReport)) return false;
+            final DataStallReport that = (DataStallReport) o;
+
+            // PersistableBundle is optimized to avoid unparcelling data unless fields are
+            // referenced. Because of this, use {@link ConnectivityDiagnosticsManager#equals} over
+            // {@link PersistableBundle#kindofEquals}.
+            return mReportTimestamp == that.mReportTimestamp
+                    && mDetectionMethod == that.mDetectionMethod
+                    && mNetwork.equals(that.mNetwork)
+                    && mLinkProperties.equals(that.mLinkProperties)
+                    && mNetworkCapabilities.equals(that.mNetworkCapabilities)
+                    && persistableBundleEquals(mStallDetails, that.mStallDetails);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(
+                    mNetwork,
+                    mReportTimestamp,
+                    mDetectionMethod,
+                    mLinkProperties,
+                    mNetworkCapabilities,
+                    mStallDetails);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, int flags) {
+            dest.writeParcelable(mNetwork, flags);
+            dest.writeLong(mReportTimestamp);
+            dest.writeInt(mDetectionMethod);
+            dest.writeParcelable(mLinkProperties, flags);
+            dest.writeParcelable(mNetworkCapabilities, flags);
+            dest.writeParcelable(mStallDetails, flags);
+        }
+
+        /** Implement the Parcelable interface */
+        public static final @NonNull Creator<DataStallReport> CREATOR =
+                new Creator<DataStallReport>() {
+                    public DataStallReport createFromParcel(Parcel in) {
+                        return new DataStallReport(
+                                in.readParcelable(null),
+                                in.readLong(),
+                                in.readInt(),
+                                in.readParcelable(null),
+                                in.readParcelable(null),
+                                in.readParcelable(null));
+                    }
+
+                    public DataStallReport[] newArray(int size) {
+                        return new DataStallReport[size];
+                    }
+                };
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public static class ConnectivityDiagnosticsBinder
+            extends IConnectivityDiagnosticsCallback.Stub {
+        @NonNull private final ConnectivityDiagnosticsCallback mCb;
+        @NonNull private final Executor mExecutor;
+
+        /** @hide */
+        @VisibleForTesting
+        public ConnectivityDiagnosticsBinder(
+                @NonNull ConnectivityDiagnosticsCallback cb, @NonNull Executor executor) {
+            this.mCb = cb;
+            this.mExecutor = executor;
+        }
+
+        /** @hide */
+        @VisibleForTesting
+        public void onConnectivityReportAvailable(@NonNull ConnectivityReport report) {
+            final long token = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> {
+                    mCb.onConnectivityReportAvailable(report);
+                });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        /** @hide */
+        @VisibleForTesting
+        public void onDataStallSuspected(@NonNull DataStallReport report) {
+            final long token = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> {
+                    mCb.onDataStallSuspected(report);
+                });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        /** @hide */
+        @VisibleForTesting
+        public void onNetworkConnectivityReported(
+                @NonNull Network network, boolean hasConnectivity) {
+            final long token = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> {
+                    mCb.onNetworkConnectivityReported(network, hasConnectivity);
+                });
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+    }
+
+    /**
+     * Abstract base class for Connectivity Diagnostics callbacks. Used for notifications about
+     * network connectivity events. Must be extended by applications wanting notifications.
+     */
+    public abstract static class ConnectivityDiagnosticsCallback {
+        /**
+         * Called when the platform completes a data connectivity check. This will also be invoked
+         * immediately upon registration for each network matching the request with the latest
+         * report, if a report has already been generated for that network.
+         *
+         * <p>The Network specified in the ConnectivityReport may not be active any more when this
+         * method is invoked.
+         *
+         * @param report The ConnectivityReport containing information about a connectivity check
+         */
+        public void onConnectivityReportAvailable(@NonNull ConnectivityReport report) {}
+
+        /**
+         * Called when the platform suspects a data stall on some Network.
+         *
+         * <p>The Network specified in the DataStallReport may not be active any more when this
+         * method is invoked.
+         *
+         * @param report The DataStallReport containing information about the suspected data stall
+         */
+        public void onDataStallSuspected(@NonNull DataStallReport report) {}
+
+        /**
+         * Called when any app reports connectivity to the System.
+         *
+         * @param network The Network for which connectivity has been reported
+         * @param hasConnectivity The connectivity reported to the System
+         */
+        public void onNetworkConnectivityReported(
+                @NonNull Network network, boolean hasConnectivity) {}
+    }
+
+    /**
+     * Registers a ConnectivityDiagnosticsCallback with the System.
+     *
+     * <p>Only apps that offer network connectivity to the user should be registering callbacks.
+     * These are the only apps whose callbacks will be invoked by the system. Apps considered to
+     * meet these conditions include:
+     *
+     * <ul>
+     *   <li>Carrier apps with active subscriptions
+     *   <li>Active VPNs
+     *   <li>WiFi Suggesters
+     * </ul>
+     *
+     * <p>Callbacks registered by apps not meeting the above criteria will not be invoked.
+     *
+     * <p>If a registering app loses its relevant permissions, any callbacks it registered will
+     * silently stop receiving callbacks. Note that registering apps must also have location
+     * permissions to receive callbacks as some Networks may be location-bound (such as WiFi
+     * networks).
+     *
+     * <p>Each register() call <b>MUST</b> use a ConnectivityDiagnosticsCallback instance that is
+     * not currently registered. If a ConnectivityDiagnosticsCallback instance is registered with
+     * multiple NetworkRequests, an IllegalArgumentException will be thrown.
+     *
+     * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+     * number of outstanding requests to 100 per app (identified by their UID), shared with
+     * callbacks in {@link ConnectivityManager}. Registering a callback with this method will count
+     * toward this limit. If this limit is exceeded, an exception will be thrown. To avoid hitting
+     * this issue and to conserve resources, make sure to unregister the callbacks with
+     * {@link #unregisterConnectivityDiagnosticsCallback}.
+     *
+     * @param request The NetworkRequest that will be used to match with Networks for which
+     *     callbacks will be fired
+     * @param e The Executor to be used for running the callback method invocations
+     * @param callback The ConnectivityDiagnosticsCallback that the caller wants registered with the
+     *     System
+     * @throws IllegalArgumentException if the same callback instance is registered with multiple
+     *     NetworkRequests
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     */
+    public void registerConnectivityDiagnosticsCallback(
+            @NonNull NetworkRequest request,
+            @NonNull Executor e,
+            @NonNull ConnectivityDiagnosticsCallback callback) {
+        final ConnectivityDiagnosticsBinder binder = new ConnectivityDiagnosticsBinder(callback, e);
+        if (sCallbacks.putIfAbsent(callback, binder) != null) {
+            throw new IllegalArgumentException("Callback is currently registered");
+        }
+
+        try {
+            mService.registerConnectivityDiagnosticsCallback(
+                    binder, request, mContext.getOpPackageName());
+        } catch (RemoteException exception) {
+            exception.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Unregisters a ConnectivityDiagnosticsCallback with the System.
+     *
+     * <p>If the given callback is not currently registered with the System, this operation will be
+     * a no-op.
+     *
+     * @param callback The ConnectivityDiagnosticsCallback to be unregistered from the System.
+     */
+    public void unregisterConnectivityDiagnosticsCallback(
+            @NonNull ConnectivityDiagnosticsCallback callback) {
+        // unconditionally removing from sCallbacks prevents race conditions here, since remove() is
+        // atomic.
+        final ConnectivityDiagnosticsBinder binder = sCallbacks.remove(callback);
+        if (binder == null) return;
+
+        try {
+            mService.unregisterConnectivityDiagnosticsCallback(binder);
+        } catch (RemoteException exception) {
+            exception.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/framework/src/android/net/ConnectivityFrameworkInitializer.java b/framework/src/android/net/ConnectivityFrameworkInitializer.java
new file mode 100644
index 0000000..a2e218d
--- /dev/null
+++ b/framework/src/android/net/ConnectivityFrameworkInitializer.java
@@ -0,0 +1,78 @@
+/*
+ * 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.content.Context;
+
+/**
+ * Class for performing registration for all core connectivity services.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class ConnectivityFrameworkInitializer {
+    private ConnectivityFrameworkInitializer() {}
+
+    /**
+     * Called by {@link SystemServiceRegistry}'s static initializer and registers all core
+     * connectivity 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() {
+        // registerContextAwareService will throw if this is called outside of SystemServiceRegistry
+        // initialization.
+        SystemServiceRegistry.registerContextAwareService(
+                Context.CONNECTIVITY_SERVICE,
+                ConnectivityManager.class,
+                (context, serviceBinder) -> {
+                    IConnectivityManager icm = IConnectivityManager.Stub.asInterface(serviceBinder);
+                    return new ConnectivityManager(context, icm);
+                }
+        );
+
+        SystemServiceRegistry.registerContextAwareService(
+                Context.CONNECTIVITY_DIAGNOSTICS_SERVICE,
+                ConnectivityDiagnosticsManager.class,
+                (context) -> {
+                    final ConnectivityManager cm = context.getSystemService(
+                            ConnectivityManager.class);
+                    return cm.createDiagnosticsManager();
+                }
+        );
+
+        SystemServiceRegistry.registerContextAwareService(
+                Context.TEST_NETWORK_SERVICE,
+                TestNetworkManager.class,
+                context -> {
+                    final ConnectivityManager cm = context.getSystemService(
+                            ConnectivityManager.class);
+                    return cm.startOrGetTestNetworkManager();
+                }
+        );
+
+        SystemServiceRegistry.registerContextAwareService(
+                DnsResolverServiceManager.DNS_RESOLVER_SERVICE,
+                DnsResolverServiceManager.class,
+                (context, serviceBinder) -> new DnsResolverServiceManager(serviceBinder)
+        );
+    }
+}
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
new file mode 100644
index 0000000..b8f8aae
--- /dev/null
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -0,0 +1,5477 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.NetworkRequest.Type.BACKGROUND_REQUEST;
+import static android.net.NetworkRequest.Type.LISTEN;
+import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST;
+import static android.net.NetworkRequest.Type.REQUEST;
+import static android.net.NetworkRequest.Type.TRACK_DEFAULT;
+import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT;
+import static android.net.QosCallback.QosCallbackRegistrationException;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityDiagnosticsManager.DataStallReport.DetectionMethod;
+import android.net.IpSecManager.UdpEncapsulationSocket;
+import android.net.SocketKeepalive.Callback;
+import android.net.TetheringManager.StartTetheringCallback;
+import android.net.TetheringManager.TetheringEventCallback;
+import android.net.TetheringManager.TetheringRequest;
+import android.net.wifi.WifiNetworkSuggestion;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ServiceSpecificException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Range;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+import libcore.net.event.NetworkEventDispatcher;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * Class that answers queries about the state of network connectivity. It also
+ * notifies applications when network connectivity changes.
+ * <p>
+ * The primary responsibilities of this class are to:
+ * <ol>
+ * <li>Monitor network connections (Wi-Fi, GPRS, UMTS, etc.)</li>
+ * <li>Send broadcast intents when network connectivity changes</li>
+ * <li>Attempt to "fail over" to another network when connectivity to a network
+ * is lost</li>
+ * <li>Provide an API that allows applications to query the coarse-grained or fine-grained
+ * state of the available networks</li>
+ * <li>Provide an API that allows applications to request and select networks for their data
+ * traffic</li>
+ * </ol>
+ */
+@SystemService(Context.CONNECTIVITY_SERVICE)
+public class ConnectivityManager {
+    private static final String TAG = "ConnectivityManager";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    /**
+     * A change in network connectivity has occurred. A default connection has either
+     * been established or lost. The NetworkInfo for the affected network is
+     * sent as an extra; it should be consulted to see what kind of
+     * connectivity event occurred.
+     * <p/>
+     * Apps targeting Android 7.0 (API level 24) and higher do not receive this
+     * broadcast if they declare the broadcast receiver in their manifest. Apps
+     * will still receive broadcasts if they register their
+     * {@link android.content.BroadcastReceiver} with
+     * {@link android.content.Context#registerReceiver Context.registerReceiver()}
+     * and that context is still valid.
+     * <p/>
+     * If this is a connection that was the result of failing over from a
+     * disconnected network, then the FAILOVER_CONNECTION boolean extra is
+     * set to true.
+     * <p/>
+     * For a loss of connectivity, if the connectivity manager is attempting
+     * to connect (or has already connected) to another network, the
+     * NetworkInfo for the new network is also passed as an extra. This lets
+     * any receivers of the broadcast know that they should not necessarily
+     * tell the user that no data traffic will be possible. Instead, the
+     * receiver should expect another broadcast soon, indicating either that
+     * the failover attempt succeeded (and so there is still overall data
+     * connectivity), or that the failover attempt failed, meaning that all
+     * connectivity has been lost.
+     * <p/>
+     * For a disconnect event, the boolean extra EXTRA_NO_CONNECTIVITY
+     * is set to {@code true} if there are no connected networks at all.
+     *
+     * @deprecated apps should use the more versatile {@link #requestNetwork},
+     *             {@link #registerNetworkCallback} or {@link #registerDefaultNetworkCallback}
+     *             functions instead for faster and more detailed updates about the network
+     *             changes they care about.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    @Deprecated
+    public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
+
+    /**
+     * The device has connected to a network that has presented a captive
+     * portal, which is blocking Internet connectivity. The user was presented
+     * with a notification that network sign in is required,
+     * and the user invoked the notification's action indicating they
+     * desire to sign in to the network. Apps handling this activity should
+     * facilitate signing in to the network. This action includes a
+     * {@link Network} typed extra called {@link #EXTRA_NETWORK} that represents
+     * the network presenting the captive portal; all communication with the
+     * captive portal must be done using this {@code Network} object.
+     * <p/>
+     * This activity includes a {@link CaptivePortal} extra named
+     * {@link #EXTRA_CAPTIVE_PORTAL} that can be used to indicate different
+     * outcomes of the captive portal sign in to the system:
+     * <ul>
+     * <li> When the app handling this action believes the user has signed in to
+     * the network and the captive portal has been dismissed, the app should
+     * call {@link CaptivePortal#reportCaptivePortalDismissed} so the system can
+     * reevaluate the network. If reevaluation finds the network no longer
+     * subject to a captive portal, the network may become the default active
+     * data network.</li>
+     * <li> When the app handling this action believes the user explicitly wants
+     * to ignore the captive portal and the network, the app should call
+     * {@link CaptivePortal#ignoreNetwork}. </li>
+     * </ul>
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_CAPTIVE_PORTAL_SIGN_IN = "android.net.conn.CAPTIVE_PORTAL";
+
+    /**
+     * The lookup key for a {@link NetworkInfo} object. Retrieve with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     *
+     * @deprecated The {@link NetworkInfo} object is deprecated, as many of its properties
+     *             can't accurately represent modern network characteristics.
+     *             Please obtain information about networks from the {@link NetworkCapabilities}
+     *             or {@link LinkProperties} objects instead.
+     */
+    @Deprecated
+    public static final String EXTRA_NETWORK_INFO = "networkInfo";
+
+    /**
+     * Network type which triggered a {@link #CONNECTIVITY_ACTION} broadcast.
+     *
+     * @see android.content.Intent#getIntExtra(String, int)
+     * @deprecated The network type is not rich enough to represent the characteristics
+     *             of modern networks. Please use {@link NetworkCapabilities} instead,
+     *             in particular the transports.
+     */
+    @Deprecated
+    public static final String EXTRA_NETWORK_TYPE = "networkType";
+
+    /**
+     * The lookup key for a boolean that indicates whether a connect event
+     * is for a network to which the connectivity manager was failing over
+     * following a disconnect on another network.
+     * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+     *
+     * @deprecated See {@link NetworkInfo}.
+     */
+    @Deprecated
+    public static final String EXTRA_IS_FAILOVER = "isFailover";
+    /**
+     * The lookup key for a {@link NetworkInfo} object. This is supplied when
+     * there is another network that it may be possible to connect to. Retrieve with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     *
+     * @deprecated See {@link NetworkInfo}.
+     */
+    @Deprecated
+    public static final String EXTRA_OTHER_NETWORK_INFO = "otherNetwork";
+    /**
+     * The lookup key for a boolean that indicates whether there is a
+     * complete lack of connectivity, i.e., no network is available.
+     * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+     */
+    public static final String EXTRA_NO_CONNECTIVITY = "noConnectivity";
+    /**
+     * The lookup key for a string that indicates why an attempt to connect
+     * to a network failed. The string has no particular structure. It is
+     * intended to be used in notifications presented to users. Retrieve
+     * it with {@link android.content.Intent#getStringExtra(String)}.
+     */
+    public static final String EXTRA_REASON = "reason";
+    /**
+     * The lookup key for a string that provides optionally supplied
+     * extra information about the network state. The information
+     * may be passed up from the lower networking layers, and its
+     * meaning may be specific to a particular network type. Retrieve
+     * it with {@link android.content.Intent#getStringExtra(String)}.
+     *
+     * @deprecated See {@link NetworkInfo#getExtraInfo()}.
+     */
+    @Deprecated
+    public static final String EXTRA_EXTRA_INFO = "extraInfo";
+    /**
+     * The lookup key for an int that provides information about
+     * our connection to the internet at large.  0 indicates no connection,
+     * 100 indicates a great connection.  Retrieve it with
+     * {@link android.content.Intent#getIntExtra(String, int)}.
+     * {@hide}
+     */
+    public static final String EXTRA_INET_CONDITION = "inetCondition";
+    /**
+     * The lookup key for a {@link CaptivePortal} object included with the
+     * {@link #ACTION_CAPTIVE_PORTAL_SIGN_IN} intent.  The {@code CaptivePortal}
+     * object can be used to either indicate to the system that the captive
+     * portal has been dismissed or that the user does not want to pursue
+     * signing in to captive portal.  Retrieve it with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     */
+    public static final String EXTRA_CAPTIVE_PORTAL = "android.net.extra.CAPTIVE_PORTAL";
+
+    /**
+     * Key for passing a URL to the captive portal login activity.
+     */
+    public static final String EXTRA_CAPTIVE_PORTAL_URL = "android.net.extra.CAPTIVE_PORTAL_URL";
+
+    /**
+     * Key for passing a {@link android.net.captiveportal.CaptivePortalProbeSpec} to the captive
+     * portal login activity.
+     * {@hide}
+     */
+    @SystemApi
+    public static final String EXTRA_CAPTIVE_PORTAL_PROBE_SPEC =
+            "android.net.extra.CAPTIVE_PORTAL_PROBE_SPEC";
+
+    /**
+     * Key for passing a user agent string to the captive portal login activity.
+     * {@hide}
+     */
+    @SystemApi
+    public static final String EXTRA_CAPTIVE_PORTAL_USER_AGENT =
+            "android.net.extra.CAPTIVE_PORTAL_USER_AGENT";
+
+    /**
+     * Broadcast action to indicate the change of data activity status
+     * (idle or active) on a network in a recent period.
+     * The network becomes active when data transmission is started, or
+     * idle if there is no data transmission for a period of time.
+     * {@hide}
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_DATA_ACTIVITY_CHANGE =
+            "android.net.conn.DATA_ACTIVITY_CHANGE";
+    /**
+     * The lookup key for an enum that indicates the network device type on which this data activity
+     * change happens.
+     * {@hide}
+     */
+    public static final String EXTRA_DEVICE_TYPE = "deviceType";
+    /**
+     * The lookup key for a boolean that indicates the device is active or not. {@code true} means
+     * it is actively sending or receiving data and {@code false} means it is idle.
+     * {@hide}
+     */
+    public static final String EXTRA_IS_ACTIVE = "isActive";
+    /**
+     * The lookup key for a long that contains the timestamp (nanos) of the radio state change.
+     * {@hide}
+     */
+    public static final String EXTRA_REALTIME_NS = "tsNanos";
+
+    /**
+     * Broadcast Action: The setting for background data usage has changed
+     * values. Use {@link #getBackgroundDataSetting()} to get the current value.
+     * <p>
+     * If an application uses the network in the background, it should listen
+     * for this broadcast and stop using the background data if the value is
+     * {@code false}.
+     * <p>
+     *
+     * @deprecated As of {@link VERSION_CODES#ICE_CREAM_SANDWICH}, availability
+     *             of background data depends on several combined factors, and
+     *             this broadcast is no longer sent. Instead, when background
+     *             data is unavailable, {@link #getActiveNetworkInfo()} will now
+     *             appear disconnected. During first boot after a platform
+     *             upgrade, this broadcast will be sent once if
+     *             {@link #getBackgroundDataSetting()} was {@code false} before
+     *             the upgrade.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    @Deprecated
+    public static final String ACTION_BACKGROUND_DATA_SETTING_CHANGED =
+            "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED";
+
+    /**
+     * Broadcast Action: The network connection may not be good
+     * uses {@code ConnectivityManager.EXTRA_INET_CONDITION} and
+     * {@code ConnectivityManager.EXTRA_NETWORK_INFO} to specify
+     * the network and it's condition.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    @UnsupportedAppUsage
+    public static final String INET_CONDITION_ACTION =
+            "android.net.conn.INET_CONDITION_ACTION";
+
+    /**
+     * Broadcast Action: A tetherable connection has come or gone.
+     * Uses {@code ConnectivityManager.EXTRA_AVAILABLE_TETHER},
+     * {@code ConnectivityManager.EXTRA_ACTIVE_LOCAL_ONLY},
+     * {@code ConnectivityManager.EXTRA_ACTIVE_TETHER}, and
+     * {@code ConnectivityManager.EXTRA_ERRORED_TETHER} to indicate
+     * the current state of tethering.  Each include a list of
+     * interface names in that state (may be empty).
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static final String ACTION_TETHER_STATE_CHANGED =
+            TetheringManager.ACTION_TETHER_STATE_CHANGED;
+
+    /**
+     * @hide
+     * gives a String[] listing all the interfaces configured for
+     * tethering and currently available for tethering.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static final String EXTRA_AVAILABLE_TETHER = TetheringManager.EXTRA_AVAILABLE_TETHER;
+
+    /**
+     * @hide
+     * gives a String[] listing all the interfaces currently in local-only
+     * mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding)
+     */
+    public static final String EXTRA_ACTIVE_LOCAL_ONLY = TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY;
+
+    /**
+     * @hide
+     * gives a String[] listing all the interfaces currently tethered
+     * (ie, has DHCPv4 support and packets potentially forwarded/NATed)
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static final String EXTRA_ACTIVE_TETHER = TetheringManager.EXTRA_ACTIVE_TETHER;
+
+    /**
+     * @hide
+     * gives a String[] listing all the interfaces we tried to tether and
+     * failed.  Use {@link #getLastTetherError} to find the error code
+     * for any interfaces listed here.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static final String EXTRA_ERRORED_TETHER = TetheringManager.EXTRA_ERRORED_TETHER;
+
+    /**
+     * Broadcast Action: The captive portal tracker has finished its test.
+     * Sent only while running Setup Wizard, in lieu of showing a user
+     * notification.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_CAPTIVE_PORTAL_TEST_COMPLETED =
+            "android.net.conn.CAPTIVE_PORTAL_TEST_COMPLETED";
+    /**
+     * The lookup key for a boolean that indicates whether a captive portal was detected.
+     * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+     * @hide
+     */
+    public static final String EXTRA_IS_CAPTIVE_PORTAL = "captivePortal";
+
+    /**
+     * Action used to display a dialog that asks the user whether to connect to a network that is
+     * not validated. This intent is used to start the dialog in settings via startActivity.
+     *
+     * This action includes a {@link Network} typed extra which is called
+     * {@link ConnectivityManager#EXTRA_NETWORK} that represents the network which is unvalidated.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final String ACTION_PROMPT_UNVALIDATED = "android.net.action.PROMPT_UNVALIDATED";
+
+    /**
+     * Action used to display a dialog that asks the user whether to avoid a network that is no
+     * longer validated. This intent is used to start the dialog in settings via startActivity.
+     *
+     * This action includes a {@link Network} typed extra which is called
+     * {@link ConnectivityManager#EXTRA_NETWORK} that represents the network which is no longer
+     * validated.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final String ACTION_PROMPT_LOST_VALIDATION =
+            "android.net.action.PROMPT_LOST_VALIDATION";
+
+    /**
+     * Action used to display a dialog that asks the user whether to stay connected to a network
+     * that has not validated. This intent is used to start the dialog in settings via
+     * startActivity.
+     *
+     * This action includes a {@link Network} typed extra which is called
+     * {@link ConnectivityManager#EXTRA_NETWORK} that represents the network which has partial
+     * connectivity.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final String ACTION_PROMPT_PARTIAL_CONNECTIVITY =
+            "android.net.action.PROMPT_PARTIAL_CONNECTIVITY";
+
+    /**
+     * Clear DNS Cache Action: This is broadcast when networks have changed and old
+     * DNS entries should be cleared.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final String ACTION_CLEAR_DNS_CACHE = "android.net.action.CLEAR_DNS_CACHE";
+
+    /**
+     * Invalid tethering type.
+     * @see #startTethering(int, boolean, OnStartTetheringCallback)
+     * @hide
+     */
+    public static final int TETHERING_INVALID   = TetheringManager.TETHERING_INVALID;
+
+    /**
+     * Wifi tethering type.
+     * @see #startTethering(int, boolean, OnStartTetheringCallback)
+     * @hide
+     */
+    @SystemApi
+    public static final int TETHERING_WIFI      = 0;
+
+    /**
+     * USB tethering type.
+     * @see #startTethering(int, boolean, OnStartTetheringCallback)
+     * @hide
+     */
+    @SystemApi
+    public static final int TETHERING_USB       = 1;
+
+    /**
+     * Bluetooth tethering type.
+     * @see #startTethering(int, boolean, OnStartTetheringCallback)
+     * @hide
+     */
+    @SystemApi
+    public static final int TETHERING_BLUETOOTH = 2;
+
+    /**
+     * Wifi P2p tethering type.
+     * Wifi P2p tethering is set through events automatically, and don't
+     * need to start from #startTethering(int, boolean, OnStartTetheringCallback).
+     * @hide
+     */
+    public static final int TETHERING_WIFI_P2P = TetheringManager.TETHERING_WIFI_P2P;
+
+    /**
+     * Extra used for communicating with the TetherService. Includes the type of tethering to
+     * enable if any.
+     * @hide
+     */
+    public static final String EXTRA_ADD_TETHER_TYPE = TetheringConstants.EXTRA_ADD_TETHER_TYPE;
+
+    /**
+     * Extra used for communicating with the TetherService. Includes the type of tethering for
+     * which to cancel provisioning.
+     * @hide
+     */
+    public static final String EXTRA_REM_TETHER_TYPE = TetheringConstants.EXTRA_REM_TETHER_TYPE;
+
+    /**
+     * Extra used for communicating with the TetherService. True to schedule a recheck of tether
+     * provisioning.
+     * @hide
+     */
+    public static final String EXTRA_SET_ALARM = TetheringConstants.EXTRA_SET_ALARM;
+
+    /**
+     * Tells the TetherService to run a provision check now.
+     * @hide
+     */
+    public static final String EXTRA_RUN_PROVISION = TetheringConstants.EXTRA_RUN_PROVISION;
+
+    /**
+     * Extra used for communicating with the TetherService. Contains the {@link ResultReceiver}
+     * which will receive provisioning results. Can be left empty.
+     * @hide
+     */
+    public static final String EXTRA_PROVISION_CALLBACK =
+            TetheringConstants.EXTRA_PROVISION_CALLBACK;
+
+    /**
+     * The absence of a connection type.
+     * @hide
+     */
+    @SystemApi
+    public static final int TYPE_NONE        = -1;
+
+    /**
+     * A Mobile data connection. Devices may support more than one.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
+     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     */
+    @Deprecated
+    public static final int TYPE_MOBILE      = 0;
+
+    /**
+     * A WIFI data connection. Devices may support more than one.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
+     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     */
+    @Deprecated
+    public static final int TYPE_WIFI        = 1;
+
+    /**
+     * An MMS-specific Mobile data connection.  This network type may use the
+     * same network interface as {@link #TYPE_MOBILE} or it may use a different
+     * one.  This is used by applications needing to talk to the carrier's
+     * Multimedia Messaging Service servers.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasCapability} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request a network that
+     *         provides the {@link NetworkCapabilities#NET_CAPABILITY_MMS} capability.
+     */
+    @Deprecated
+    public static final int TYPE_MOBILE_MMS  = 2;
+
+    /**
+     * A SUPL-specific Mobile data connection.  This network type may use the
+     * same network interface as {@link #TYPE_MOBILE} or it may use a different
+     * one.  This is used by applications needing to talk to the carrier's
+     * Secure User Plane Location servers for help locating the device.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasCapability} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request a network that
+     *         provides the {@link NetworkCapabilities#NET_CAPABILITY_SUPL} capability.
+     */
+    @Deprecated
+    public static final int TYPE_MOBILE_SUPL = 3;
+
+    /**
+     * A DUN-specific Mobile data connection.  This network type may use the
+     * same network interface as {@link #TYPE_MOBILE} or it may use a different
+     * one.  This is sometimes by the system when setting up an upstream connection
+     * for tethering so that the carrier is aware of DUN traffic.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasCapability} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request a network that
+     *         provides the {@link NetworkCapabilities#NET_CAPABILITY_DUN} capability.
+     */
+    @Deprecated
+    public static final int TYPE_MOBILE_DUN  = 4;
+
+    /**
+     * A High Priority Mobile data connection.  This network type uses the
+     * same network interface as {@link #TYPE_MOBILE} but the routing setup
+     * is different.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
+     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     */
+    @Deprecated
+    public static final int TYPE_MOBILE_HIPRI = 5;
+
+    /**
+     * A WiMAX data connection.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
+     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     */
+    @Deprecated
+    public static final int TYPE_WIMAX       = 6;
+
+    /**
+     * A Bluetooth data connection.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
+     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     */
+    @Deprecated
+    public static final int TYPE_BLUETOOTH   = 7;
+
+    /**
+     * Fake data connection.  This should not be used on shipping devices.
+     * @deprecated This is not used any more.
+     */
+    @Deprecated
+    public static final int TYPE_DUMMY       = 8;
+
+    /**
+     * An Ethernet data connection.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
+     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     */
+    @Deprecated
+    public static final int TYPE_ETHERNET    = 9;
+
+    /**
+     * Over the air Administration.
+     * @deprecated Use {@link NetworkCapabilities} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public static final int TYPE_MOBILE_FOTA = 10;
+
+    /**
+     * IP Multimedia Subsystem.
+     * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_IMS} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static final int TYPE_MOBILE_IMS  = 11;
+
+    /**
+     * Carrier Branded Services.
+     * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_CBS} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public static final int TYPE_MOBILE_CBS  = 12;
+
+    /**
+     * A Wi-Fi p2p connection. Only requesting processes will have access to
+     * the peers connected.
+     * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_WIFI_P2P} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @SystemApi
+    public static final int TYPE_WIFI_P2P    = 13;
+
+    /**
+     * The network to use for initially attaching to the network
+     * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_IA} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static final int TYPE_MOBILE_IA = 14;
+
+    /**
+     * Emergency PDN connection for emergency services.  This
+     * may include IMS and MMS in emergency situations.
+     * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_EIMS} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public static final int TYPE_MOBILE_EMERGENCY = 15;
+
+    /**
+     * The network that uses proxy to achieve connectivity.
+     * @deprecated Use {@link NetworkCapabilities} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @SystemApi
+    public static final int TYPE_PROXY = 16;
+
+    /**
+     * A virtual network using one or more native bearers.
+     * It may or may not be providing security services.
+     * @deprecated Applications should use {@link NetworkCapabilities#TRANSPORT_VPN} instead.
+     */
+    @Deprecated
+    public static final int TYPE_VPN = 17;
+
+    /**
+     * A network that is exclusively meant to be used for testing
+     *
+     * @deprecated Use {@link NetworkCapabilities} instead.
+     * @hide
+     */
+    @Deprecated
+    public static final int TYPE_TEST = 18; // TODO: Remove this once NetworkTypes are unused.
+
+    /**
+     * @deprecated Use {@link NetworkCapabilities} instead.
+     * @hide
+     */
+    @Deprecated
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "TYPE_" }, value = {
+                TYPE_NONE,
+                TYPE_MOBILE,
+                TYPE_WIFI,
+                TYPE_MOBILE_MMS,
+                TYPE_MOBILE_SUPL,
+                TYPE_MOBILE_DUN,
+                TYPE_MOBILE_HIPRI,
+                TYPE_WIMAX,
+                TYPE_BLUETOOTH,
+                TYPE_DUMMY,
+                TYPE_ETHERNET,
+                TYPE_MOBILE_FOTA,
+                TYPE_MOBILE_IMS,
+                TYPE_MOBILE_CBS,
+                TYPE_WIFI_P2P,
+                TYPE_MOBILE_IA,
+                TYPE_MOBILE_EMERGENCY,
+                TYPE_PROXY,
+                TYPE_VPN,
+                TYPE_TEST
+    })
+    public @interface LegacyNetworkType {}
+
+    // Deprecated constants for return values of startUsingNetworkFeature. They used to live
+    // in com.android.internal.telephony.PhoneConstants until they were made inaccessible.
+    private static final int DEPRECATED_PHONE_CONSTANT_APN_ALREADY_ACTIVE = 0;
+    private static final int DEPRECATED_PHONE_CONSTANT_APN_REQUEST_STARTED = 1;
+    private static final int DEPRECATED_PHONE_CONSTANT_APN_REQUEST_FAILED = 3;
+
+    /** {@hide} */
+    public static final int MAX_RADIO_TYPE = TYPE_TEST;
+
+    /** {@hide} */
+    public static final int MAX_NETWORK_TYPE = TYPE_TEST;
+
+    private static final int MIN_NETWORK_TYPE = TYPE_MOBILE;
+
+    /**
+     * If you want to set the default network preference,you can directly
+     * change the networkAttributes array in framework's config.xml.
+     *
+     * @deprecated Since we support so many more networks now, the single
+     *             network default network preference can't really express
+     *             the hierarchy.  Instead, the default is defined by the
+     *             networkAttributes in config.xml.  You can determine
+     *             the current value by calling {@link #getNetworkPreference()}
+     *             from an App.
+     */
+    @Deprecated
+    public static final int DEFAULT_NETWORK_PREFERENCE = TYPE_WIFI;
+
+    /**
+     * @hide
+     */
+    public static final int REQUEST_ID_UNSET = 0;
+
+    /**
+     * Static unique request used as a tombstone for NetworkCallbacks that have been unregistered.
+     * This allows to distinguish when unregistering NetworkCallbacks those that were never
+     * registered from those that were already unregistered.
+     * @hide
+     */
+    private static final NetworkRequest ALREADY_UNREGISTERED =
+            new NetworkRequest.Builder().clearCapabilities().build();
+
+    /**
+     * A NetID indicating no Network is selected.
+     * Keep in sync with bionic/libc/dns/include/resolv_netid.h
+     * @hide
+     */
+    public static final int NETID_UNSET = 0;
+
+    /**
+     * Flag to indicate that an app is not subject to any restrictions that could result in its
+     * network access blocked.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_REASON_NONE = 0;
+
+    /**
+     * Flag to indicate that an app is subject to Battery saver restrictions that would
+     * result in its network access being blocked.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_REASON_BATTERY_SAVER = 1 << 0;
+
+    /**
+     * Flag to indicate that an app is subject to Doze restrictions that would
+     * result in its network access being blocked.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_REASON_DOZE = 1 << 1;
+
+    /**
+     * Flag to indicate that an app is subject to App Standby restrictions that would
+     * result in its network access being blocked.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_REASON_APP_STANDBY = 1 << 2;
+
+    /**
+     * Flag to indicate that an app is subject to Restricted mode restrictions that would
+     * result in its network access being blocked.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_REASON_RESTRICTED_MODE = 1 << 3;
+
+    /**
+     * Flag to indicate that an app is blocked because it is subject to an always-on VPN but the VPN
+     * is not currently connected.
+     *
+     * @see DevicePolicyManager#setAlwaysOnVpnPackage(ComponentName, String, boolean)
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_REASON_LOCKDOWN_VPN = 1 << 4;
+
+    /**
+     * Flag to indicate that an app is subject to Data saver restrictions that would
+     * result in its metered network access being blocked.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_METERED_REASON_DATA_SAVER = 1 << 16;
+
+    /**
+     * Flag to indicate that an app is subject to user restrictions that would
+     * result in its metered network access being blocked.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_METERED_REASON_USER_RESTRICTED = 1 << 17;
+
+    /**
+     * Flag to indicate that an app is subject to Device admin restrictions that would
+     * result in its metered network access being blocked.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_METERED_REASON_ADMIN_DISABLED = 1 << 18;
+
+    /**
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, prefix = {"BLOCKED_"}, value = {
+            BLOCKED_REASON_NONE,
+            BLOCKED_REASON_BATTERY_SAVER,
+            BLOCKED_REASON_DOZE,
+            BLOCKED_REASON_APP_STANDBY,
+            BLOCKED_REASON_RESTRICTED_MODE,
+            BLOCKED_REASON_LOCKDOWN_VPN,
+            BLOCKED_METERED_REASON_DATA_SAVER,
+            BLOCKED_METERED_REASON_USER_RESTRICTED,
+            BLOCKED_METERED_REASON_ADMIN_DISABLED,
+    })
+    public @interface BlockedReason {}
+
+    /**
+     * Set of blocked reasons that are only applicable on metered networks.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int BLOCKED_METERED_REASON_MASK = 0xffff0000;
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    private final IConnectivityManager mService;
+
+    /**
+     * 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
+     * methods that take a Context argument.
+     */
+    private static ConnectivityManager sInstance;
+
+    private final Context mContext;
+
+    @GuardedBy("mTetheringEventCallbacks")
+    private TetheringManager mTetheringManager;
+
+    private TetheringManager getTetheringManager() {
+        synchronized (mTetheringEventCallbacks) {
+            if (mTetheringManager == null) {
+                mTetheringManager = mContext.getSystemService(TetheringManager.class);
+            }
+            return mTetheringManager;
+        }
+    }
+
+    /**
+     * Tests if a given integer represents a valid network type.
+     * @param networkType the type to be tested
+     * @return a boolean.  {@code true} if the type is valid, else {@code false}
+     * @deprecated All APIs accepting a network type are deprecated. There should be no need to
+     *             validate a network type.
+     */
+    @Deprecated
+    public static boolean isNetworkTypeValid(int networkType) {
+        return MIN_NETWORK_TYPE <= networkType && networkType <= MAX_NETWORK_TYPE;
+    }
+
+    /**
+     * Returns a non-localized string representing a given network type.
+     * ONLY used for debugging output.
+     * @param type the type needing naming
+     * @return a String for the given type, or a string version of the type ("87")
+     * if no name is known.
+     * @deprecated Types are deprecated. Use {@link NetworkCapabilities} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static String getNetworkTypeName(int type) {
+        switch (type) {
+          case TYPE_NONE:
+                return "NONE";
+            case TYPE_MOBILE:
+                return "MOBILE";
+            case TYPE_WIFI:
+                return "WIFI";
+            case TYPE_MOBILE_MMS:
+                return "MOBILE_MMS";
+            case TYPE_MOBILE_SUPL:
+                return "MOBILE_SUPL";
+            case TYPE_MOBILE_DUN:
+                return "MOBILE_DUN";
+            case TYPE_MOBILE_HIPRI:
+                return "MOBILE_HIPRI";
+            case TYPE_WIMAX:
+                return "WIMAX";
+            case TYPE_BLUETOOTH:
+                return "BLUETOOTH";
+            case TYPE_DUMMY:
+                return "DUMMY";
+            case TYPE_ETHERNET:
+                return "ETHERNET";
+            case TYPE_MOBILE_FOTA:
+                return "MOBILE_FOTA";
+            case TYPE_MOBILE_IMS:
+                return "MOBILE_IMS";
+            case TYPE_MOBILE_CBS:
+                return "MOBILE_CBS";
+            case TYPE_WIFI_P2P:
+                return "WIFI_P2P";
+            case TYPE_MOBILE_IA:
+                return "MOBILE_IA";
+            case TYPE_MOBILE_EMERGENCY:
+                return "MOBILE_EMERGENCY";
+            case TYPE_PROXY:
+                return "PROXY";
+            case TYPE_VPN:
+                return "VPN";
+            default:
+                return Integer.toString(type);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public void systemReady() {
+        try {
+            mService.systemReady();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Checks if a given type uses the cellular data connection.
+     * This should be replaced in the future by a network property.
+     * @param networkType the type to check
+     * @return a boolean - {@code true} if uses cellular network, else {@code false}
+     * @deprecated Types are deprecated. Use {@link NetworkCapabilities} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public static boolean isNetworkTypeMobile(int networkType) {
+        switch (networkType) {
+            case TYPE_MOBILE:
+            case TYPE_MOBILE_MMS:
+            case TYPE_MOBILE_SUPL:
+            case TYPE_MOBILE_DUN:
+            case TYPE_MOBILE_HIPRI:
+            case TYPE_MOBILE_FOTA:
+            case TYPE_MOBILE_IMS:
+            case TYPE_MOBILE_CBS:
+            case TYPE_MOBILE_IA:
+            case TYPE_MOBILE_EMERGENCY:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Checks if the given network type is backed by a Wi-Fi radio.
+     *
+     * @deprecated Types are deprecated. Use {@link NetworkCapabilities} instead.
+     * @hide
+     */
+    @Deprecated
+    public static boolean isNetworkTypeWifi(int networkType) {
+        switch (networkType) {
+            case TYPE_WIFI:
+            case TYPE_WIFI_P2P:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Preference for {@link #setNetworkPreferenceForUser(UserHandle, int, Executor, Runnable)}.
+     * Specify that the traffic for this user should by follow the default rules.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int PROFILE_NETWORK_PREFERENCE_DEFAULT = 0;
+
+    /**
+     * Preference for {@link #setNetworkPreferenceForUser(UserHandle, int, Executor, Runnable)}.
+     * Specify that the traffic for this user should by default go on a network with
+     * {@link NetworkCapabilities#NET_CAPABILITY_ENTERPRISE}, and on the system default network
+     * if no such network is available.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE = 1;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            PROFILE_NETWORK_PREFERENCE_DEFAULT,
+            PROFILE_NETWORK_PREFERENCE_ENTERPRISE
+    })
+    public @interface ProfileNetworkPreference {
+    }
+
+    /**
+     * Specifies the preferred network type.  When the device has more
+     * than one type available the preferred network type will be used.
+     *
+     * @param preference the network type to prefer over all others.  It is
+     *         unspecified what happens to the old preferred network in the
+     *         overall ordering.
+     * @deprecated Functionality has been removed as it no longer makes sense,
+     *             with many more than two networks - we'd need an array to express
+     *             preference.  Instead we use dynamic network properties of
+     *             the networks to describe their precedence.
+     */
+    @Deprecated
+    public void setNetworkPreference(int preference) {
+    }
+
+    /**
+     * Retrieves the current preferred network type.
+     *
+     * @return an integer representing the preferred network type
+     *
+     * @deprecated Functionality has been removed as it no longer makes sense,
+     *             with many more than two networks - we'd need an array to express
+     *             preference.  Instead we use dynamic network properties of
+     *             the networks to describe their precedence.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public int getNetworkPreference() {
+        return TYPE_NONE;
+    }
+
+    /**
+     * Returns details about the currently active default data network. When
+     * connected, this network is the default route for outgoing connections.
+     * You should always check {@link NetworkInfo#isConnected()} before initiating
+     * network traffic. This may return {@code null} when there is no default
+     * network.
+     * Note that if the default network is a VPN, this method will return the
+     * NetworkInfo for one of its underlying networks instead, or null if the
+     * VPN agent did not specify any. Apps interested in learning about VPNs
+     * should use {@link #getNetworkInfo(android.net.Network)} instead.
+     *
+     * @return a {@link NetworkInfo} object for the current default network
+     *        or {@code null} if no default network is currently active
+     * @deprecated See {@link NetworkInfo}.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @Nullable
+    public NetworkInfo getActiveNetworkInfo() {
+        try {
+            return mService.getActiveNetworkInfo();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns a {@link Network} object corresponding to the currently active
+     * default data network.  In the event that the current active default data
+     * network disconnects, the returned {@code Network} object will no longer
+     * be usable.  This will return {@code null} when there is no default
+     * network.
+     *
+     * @return a {@link Network} object for the current default network or
+     *        {@code null} if no default network is currently active
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @Nullable
+    public Network getActiveNetwork() {
+        try {
+            return mService.getActiveNetwork();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns a {@link Network} object corresponding to the currently active
+     * default data network for a specific UID.  In the event that the default data
+     * network disconnects, the returned {@code Network} object will no longer
+     * be usable.  This will return {@code null} when there is no default
+     * network for the UID.
+     *
+     * @return a {@link Network} object for the current default network for the
+     *         given UID or {@code null} if no default network is currently active
+     * TODO: b/183465229 Cleanup getActiveNetworkForUid once b/165835257 is fixed
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+    @Nullable
+    public Network getActiveNetworkForUid(int uid) {
+        return getActiveNetworkForUid(uid, false);
+    }
+
+    /** {@hide} */
+    public Network getActiveNetworkForUid(int uid, boolean ignoreBlocked) {
+        try {
+            return mService.getActiveNetworkForUid(uid, ignoreBlocked);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Adds or removes a requirement for given UID ranges to use the VPN.
+     *
+     * If set to {@code true}, informs the system that the UIDs in the specified ranges must not
+     * have any connectivity except if a VPN is connected and applies to the UIDs, or if the UIDs
+     * otherwise have permission to bypass the VPN (e.g., because they have the
+     * {@link android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS} permission, or when
+     * using a socket protected by a method such as {@link VpnService#protect(DatagramSocket)}. If
+     * set to {@code false}, a previously-added restriction is removed.
+     * <p>
+     * Each of the UID ranges specified by this method is added and removed as is, and no processing
+     * is performed on the ranges to de-duplicate, merge, split, or intersect them. In order to
+     * remove a previously-added range, the exact range must be removed as is.
+     * <p>
+     * The changes are applied asynchronously and may not have been applied by the time the method
+     * returns. Apps will be notified about any changes that apply to them via
+     * {@link NetworkCallback#onBlockedStatusChanged} callbacks called after the changes take
+     * effect.
+     * <p>
+     * This method should be called only by the VPN code.
+     *
+     * @param ranges the UID ranges to restrict
+     * @param requireVpn whether the specified UID ranges must use a VPN
+     *
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK,
+            android.Manifest.permission.NETWORK_SETTINGS})
+    @SystemApi(client = MODULE_LIBRARIES)
+    public void setRequireVpnForUids(boolean requireVpn,
+            @NonNull Collection<Range<Integer>> ranges) {
+        Objects.requireNonNull(ranges);
+        // The Range class is not parcelable. Convert to UidRange, which is what is used internally.
+        // This method is not necessarily expected to be used outside the system server, so
+        // parceling may not be necessary, but it could be used out-of-process, e.g., by the network
+        // stack process, or by tests.
+        UidRange[] rangesArray = new UidRange[ranges.size()];
+        int index = 0;
+        for (Range<Integer> range : ranges) {
+            rangesArray[index++] = new UidRange(range.getLower(), range.getUpper());
+        }
+        try {
+            mService.setRequireVpnForUids(requireVpn, rangesArray);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Informs ConnectivityService of whether the legacy lockdown VPN, as implemented by
+     * LockdownVpnTracker, is in use. This is deprecated for new devices starting from Android 12
+     * but is still supported for backwards compatibility.
+     * <p>
+     * This type of VPN is assumed always to use the system default network, and must always declare
+     * exactly one underlying network, which is the network that was the default when the VPN
+     * connected.
+     * <p>
+     * Calling this method with {@code true} enables legacy behaviour, specifically:
+     * <ul>
+     *     <li>Any VPN that applies to userId 0 behaves specially with respect to deprecated
+     *     {@link #CONNECTIVITY_ACTION} broadcasts. Any such broadcasts will have the state in the
+     *     {@link #EXTRA_NETWORK_INFO} replaced by state of the VPN network. Also, any time the VPN
+     *     connects, a {@link #CONNECTIVITY_ACTION} broadcast will be sent for the network
+     *     underlying the VPN.</li>
+     *     <li>Deprecated APIs that return {@link NetworkInfo} objects will have their state
+     *     similarly replaced by the VPN network state.</li>
+     *     <li>Information on current network interfaces passed to NetworkStatsService will not
+     *     include any VPN interfaces.</li>
+     * </ul>
+     *
+     * @param enabled whether legacy lockdown VPN is enabled or disabled
+     *
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK,
+            android.Manifest.permission.NETWORK_SETTINGS})
+    @SystemApi(client = MODULE_LIBRARIES)
+    public void setLegacyLockdownVpnEnabled(boolean enabled) {
+        try {
+            mService.setLegacyLockdownVpnEnabled(enabled);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns details about the currently active default data network
+     * for a given uid.  This is for internal use only to avoid spying
+     * other apps.
+     *
+     * @return a {@link NetworkInfo} object for the current default network
+     *        for the given uid or {@code null} if no default network is
+     *        available for the specified uid.
+     *
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public NetworkInfo getActiveNetworkInfoForUid(int uid) {
+        return getActiveNetworkInfoForUid(uid, false);
+    }
+
+    /** {@hide} */
+    public NetworkInfo getActiveNetworkInfoForUid(int uid, boolean ignoreBlocked) {
+        try {
+            return mService.getActiveNetworkInfoForUid(uid, ignoreBlocked);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns connection status information about a particular
+     * network type.
+     *
+     * @param networkType integer specifying which networkType in
+     *        which you're interested.
+     * @return a {@link NetworkInfo} object for the requested
+     *        network type or {@code null} if the type is not
+     *        supported by the device. If {@code networkType} is
+     *        TYPE_VPN and a VPN is active for the calling app,
+     *        then this method will try to return one of the
+     *        underlying networks for the VPN or null if the
+     *        VPN agent didn't specify any.
+     *
+     * @deprecated This method does not support multiple connected networks
+     *             of the same type. Use {@link #getAllNetworks} and
+     *             {@link #getNetworkInfo(android.net.Network)} instead.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @Nullable
+    public NetworkInfo getNetworkInfo(int networkType) {
+        try {
+            return mService.getNetworkInfo(networkType);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns connection status information about a particular
+     * Network.
+     *
+     * @param network {@link Network} specifying which network
+     *        in which you're interested.
+     * @return a {@link NetworkInfo} object for the requested
+     *        network or {@code null} if the {@code Network}
+     *        is not valid.
+     * @deprecated See {@link NetworkInfo}.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @Nullable
+    public NetworkInfo getNetworkInfo(@Nullable Network network) {
+        return getNetworkInfoForUid(network, Process.myUid(), false);
+    }
+
+    /** {@hide} */
+    public NetworkInfo getNetworkInfoForUid(Network network, int uid, boolean ignoreBlocked) {
+        try {
+            return mService.getNetworkInfoForUid(network, uid, ignoreBlocked);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns connection status information about all network
+     * types supported by the device.
+     *
+     * @return an array of {@link NetworkInfo} objects.  Check each
+     * {@link NetworkInfo#getType} for which type each applies.
+     *
+     * @deprecated This method does not support multiple connected networks
+     *             of the same type. Use {@link #getAllNetworks} and
+     *             {@link #getNetworkInfo(android.net.Network)} instead.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @NonNull
+    public NetworkInfo[] getAllNetworkInfo() {
+        try {
+            return mService.getAllNetworkInfo();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return a list of {@link NetworkStateSnapshot}s, one for each network that is currently
+     * connected.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK,
+            android.Manifest.permission.NETWORK_SETTINGS})
+    @NonNull
+    public List<NetworkStateSnapshot> getAllNetworkStateSnapshots() {
+        try {
+            return mService.getAllNetworkStateSnapshots();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the {@link Network} object currently serving a given type, or
+     * null if the given type is not connected.
+     *
+     * @hide
+     * @deprecated This method does not support multiple connected networks
+     *             of the same type. Use {@link #getAllNetworks} and
+     *             {@link #getNetworkInfo(android.net.Network)} instead.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    public Network getNetworkForType(int networkType) {
+        try {
+            return mService.getNetworkForType(networkType);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns an array of all {@link Network} currently tracked by the
+     * framework.
+     *
+     * @return an array of {@link Network} objects.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @NonNull
+    public Network[] getAllNetworks() {
+        try {
+            return mService.getAllNetworks();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns an array of {@link NetworkCapabilities} objects, representing
+     * the Networks that applications run by the given user will use by default.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public NetworkCapabilities[] getDefaultNetworkCapabilitiesForUser(int userId) {
+        try {
+            return mService.getDefaultNetworkCapabilitiesForUser(
+                    userId, mContext.getOpPackageName(), getAttributionTag());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the IP information for the current default network.
+     *
+     * @return a {@link LinkProperties} object describing the IP info
+     *        for the current default network, or {@code null} if there
+     *        is no current default network.
+     *
+     * {@hide}
+     * @deprecated please use {@link #getLinkProperties(Network)} on the return
+     *             value of {@link #getActiveNetwork()} instead. In particular,
+     *             this method will return non-null LinkProperties even if the
+     *             app is blocked by policy from using this network.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 109783091)
+    public LinkProperties getActiveLinkProperties() {
+        try {
+            return mService.getActiveLinkProperties();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the IP information for a given network type.
+     *
+     * @param networkType the network type of interest.
+     * @return a {@link LinkProperties} object describing the IP info
+     *        for the given networkType, or {@code null} if there is
+     *        no current default network.
+     *
+     * {@hide}
+     * @deprecated This method does not support multiple connected networks
+     *             of the same type. Use {@link #getAllNetworks},
+     *             {@link #getNetworkInfo(android.net.Network)}, and
+     *             {@link #getLinkProperties(android.net.Network)} instead.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public LinkProperties getLinkProperties(int networkType) {
+        try {
+            return mService.getLinkPropertiesForType(networkType);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the {@link LinkProperties} for the given {@link Network}.  This
+     * will return {@code null} if the network is unknown.
+     *
+     * @param network The {@link Network} object identifying the network in question.
+     * @return The {@link LinkProperties} for the network, or {@code null}.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @Nullable
+    public LinkProperties getLinkProperties(@Nullable Network network) {
+        try {
+            return mService.getLinkProperties(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the {@link NetworkCapabilities} for the given {@link Network}.  This
+     * will return {@code null} if the network is unknown or if the |network| argument is null.
+     *
+     * This will remove any location sensitive data in {@link TransportInfo} embedded in
+     * {@link NetworkCapabilities#getTransportInfo()}. Some transport info instances like
+     * {@link android.net.wifi.WifiInfo} contain location sensitive information. Retrieving
+     * this location sensitive information (subject to app's location permissions) will be
+     * noted by system. To include any location sensitive data in {@link TransportInfo},
+     * use a {@link NetworkCallback} with
+     * {@link NetworkCallback#FLAG_INCLUDE_LOCATION_INFO} flag.
+     *
+     * @param network The {@link Network} object identifying the network in question.
+     * @return The {@link NetworkCapabilities} for the network, or {@code null}.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @Nullable
+    public NetworkCapabilities getNetworkCapabilities(@Nullable Network network) {
+        try {
+            return mService.getNetworkCapabilities(
+                    network, mContext.getOpPackageName(), getAttributionTag());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Gets a URL that can be used for resolving whether a captive portal is present.
+     * 1. This URL should respond with a 204 response to a GET request to indicate no captive
+     *    portal is present.
+     * 2. This URL must be HTTP as redirect responses are used to find captive portal
+     *    sign-in pages. Captive portals cannot respond to HTTPS requests with redirects.
+     *
+     * The system network validation may be using different strategies to detect captive portals,
+     * so this method does not necessarily return a URL used by the system. It only returns a URL
+     * that may be relevant for other components trying to detect captive portals.
+     *
+     * @hide
+     * @deprecated This API returns URL which is not guaranteed to be one of the URLs used by the
+     *             system.
+     */
+    @Deprecated
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public String getCaptivePortalServerUrl() {
+        try {
+            return mService.getCaptivePortalServerUrl();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Tells the underlying networking system that the caller wants to
+     * begin using the named feature. The interpretation of {@code feature}
+     * is completely up to each networking implementation.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param networkType specifies which network the request pertains to
+     * @param feature the name of the feature to be used
+     * @return an integer value representing the outcome of the request.
+     * The interpretation of this value is specific to each networking
+     * implementation+feature combination, except that the value {@code -1}
+     * always indicates failure.
+     *
+     * @deprecated Deprecated in favor of the cleaner
+     *             {@link #requestNetwork(NetworkRequest, NetworkCallback)} API.
+     *             In {@link VERSION_CODES#M}, and above, this method is unsupported and will
+     *             throw {@code UnsupportedOperationException} if called.
+     * @removed
+     */
+    @Deprecated
+    public int startUsingNetworkFeature(int networkType, String feature) {
+        checkLegacyRoutingApiAccess();
+        NetworkCapabilities netCap = networkCapabilitiesForFeature(networkType, feature);
+        if (netCap == null) {
+            Log.d(TAG, "Can't satisfy startUsingNetworkFeature for " + networkType + ", " +
+                    feature);
+            return DEPRECATED_PHONE_CONSTANT_APN_REQUEST_FAILED;
+        }
+
+        NetworkRequest request = null;
+        synchronized (sLegacyRequests) {
+            LegacyRequest l = sLegacyRequests.get(netCap);
+            if (l != null) {
+                Log.d(TAG, "renewing startUsingNetworkFeature request " + l.networkRequest);
+                renewRequestLocked(l);
+                if (l.currentNetwork != null) {
+                    return DEPRECATED_PHONE_CONSTANT_APN_ALREADY_ACTIVE;
+                } else {
+                    return DEPRECATED_PHONE_CONSTANT_APN_REQUEST_STARTED;
+                }
+            }
+
+            request = requestNetworkForFeatureLocked(netCap);
+        }
+        if (request != null) {
+            Log.d(TAG, "starting startUsingNetworkFeature for request " + request);
+            return DEPRECATED_PHONE_CONSTANT_APN_REQUEST_STARTED;
+        } else {
+            Log.d(TAG, " request Failed");
+            return DEPRECATED_PHONE_CONSTANT_APN_REQUEST_FAILED;
+        }
+    }
+
+    /**
+     * Tells the underlying networking system that the caller is finished
+     * using the named feature. The interpretation of {@code feature}
+     * is completely up to each networking implementation.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param networkType specifies which network the request pertains to
+     * @param feature the name of the feature that is no longer needed
+     * @return an integer value representing the outcome of the request.
+     * The interpretation of this value is specific to each networking
+     * implementation+feature combination, except that the value {@code -1}
+     * always indicates failure.
+     *
+     * @deprecated Deprecated in favor of the cleaner
+     *             {@link #unregisterNetworkCallback(NetworkCallback)} API.
+     *             In {@link VERSION_CODES#M}, and above, this method is unsupported and will
+     *             throw {@code UnsupportedOperationException} if called.
+     * @removed
+     */
+    @Deprecated
+    public int stopUsingNetworkFeature(int networkType, String feature) {
+        checkLegacyRoutingApiAccess();
+        NetworkCapabilities netCap = networkCapabilitiesForFeature(networkType, feature);
+        if (netCap == null) {
+            Log.d(TAG, "Can't satisfy stopUsingNetworkFeature for " + networkType + ", " +
+                    feature);
+            return -1;
+        }
+
+        if (removeRequestForFeature(netCap)) {
+            Log.d(TAG, "stopUsingNetworkFeature for " + networkType + ", " + feature);
+        }
+        return 1;
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private NetworkCapabilities networkCapabilitiesForFeature(int networkType, String feature) {
+        if (networkType == TYPE_MOBILE) {
+            switch (feature) {
+                case "enableCBS":
+                    return networkCapabilitiesForType(TYPE_MOBILE_CBS);
+                case "enableDUN":
+                case "enableDUNAlways":
+                    return networkCapabilitiesForType(TYPE_MOBILE_DUN);
+                case "enableFOTA":
+                    return networkCapabilitiesForType(TYPE_MOBILE_FOTA);
+                case "enableHIPRI":
+                    return networkCapabilitiesForType(TYPE_MOBILE_HIPRI);
+                case "enableIMS":
+                    return networkCapabilitiesForType(TYPE_MOBILE_IMS);
+                case "enableMMS":
+                    return networkCapabilitiesForType(TYPE_MOBILE_MMS);
+                case "enableSUPL":
+                    return networkCapabilitiesForType(TYPE_MOBILE_SUPL);
+                default:
+                    return null;
+            }
+        } else if (networkType == TYPE_WIFI && "p2p".equals(feature)) {
+            return networkCapabilitiesForType(TYPE_WIFI_P2P);
+        }
+        return null;
+    }
+
+    private int legacyTypeForNetworkCapabilities(NetworkCapabilities netCap) {
+        if (netCap == null) return TYPE_NONE;
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
+            return TYPE_MOBILE_CBS;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_IMS)) {
+            return TYPE_MOBILE_IMS;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOTA)) {
+            return TYPE_MOBILE_FOTA;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN)) {
+            return TYPE_MOBILE_DUN;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL)) {
+            return TYPE_MOBILE_SUPL;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_MMS)) {
+            return TYPE_MOBILE_MMS;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+            return TYPE_MOBILE_HIPRI;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_WIFI_P2P)) {
+            return TYPE_WIFI_P2P;
+        }
+        return TYPE_NONE;
+    }
+
+    private static class LegacyRequest {
+        NetworkCapabilities networkCapabilities;
+        NetworkRequest networkRequest;
+        int expireSequenceNumber;
+        Network currentNetwork;
+        int delay = -1;
+
+        private void clearDnsBinding() {
+            if (currentNetwork != null) {
+                currentNetwork = null;
+                setProcessDefaultNetworkForHostResolution(null);
+            }
+        }
+
+        NetworkCallback networkCallback = new NetworkCallback() {
+            @Override
+            public void onAvailable(Network network) {
+                currentNetwork = network;
+                Log.d(TAG, "startUsingNetworkFeature got Network:" + network);
+                setProcessDefaultNetworkForHostResolution(network);
+            }
+            @Override
+            public void onLost(Network network) {
+                if (network.equals(currentNetwork)) clearDnsBinding();
+                Log.d(TAG, "startUsingNetworkFeature lost Network:" + network);
+            }
+        };
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private static final HashMap<NetworkCapabilities, LegacyRequest> sLegacyRequests =
+            new HashMap<>();
+
+    private NetworkRequest findRequestForFeature(NetworkCapabilities netCap) {
+        synchronized (sLegacyRequests) {
+            LegacyRequest l = sLegacyRequests.get(netCap);
+            if (l != null) return l.networkRequest;
+        }
+        return null;
+    }
+
+    private void renewRequestLocked(LegacyRequest l) {
+        l.expireSequenceNumber++;
+        Log.d(TAG, "renewing request to seqNum " + l.expireSequenceNumber);
+        sendExpireMsgForFeature(l.networkCapabilities, l.expireSequenceNumber, l.delay);
+    }
+
+    private void expireRequest(NetworkCapabilities netCap, int sequenceNum) {
+        int ourSeqNum = -1;
+        synchronized (sLegacyRequests) {
+            LegacyRequest l = sLegacyRequests.get(netCap);
+            if (l == null) return;
+            ourSeqNum = l.expireSequenceNumber;
+            if (l.expireSequenceNumber == sequenceNum) removeRequestForFeature(netCap);
+        }
+        Log.d(TAG, "expireRequest with " + ourSeqNum + ", " + sequenceNum);
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private NetworkRequest requestNetworkForFeatureLocked(NetworkCapabilities netCap) {
+        int delay = -1;
+        int type = legacyTypeForNetworkCapabilities(netCap);
+        try {
+            delay = mService.getRestoreDefaultNetworkDelay(type);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        LegacyRequest l = new LegacyRequest();
+        l.networkCapabilities = netCap;
+        l.delay = delay;
+        l.expireSequenceNumber = 0;
+        l.networkRequest = sendRequestForNetwork(
+                netCap, l.networkCallback, 0, REQUEST, type, getDefaultHandler());
+        if (l.networkRequest == null) return null;
+        sLegacyRequests.put(netCap, l);
+        sendExpireMsgForFeature(netCap, l.expireSequenceNumber, delay);
+        return l.networkRequest;
+    }
+
+    private void sendExpireMsgForFeature(NetworkCapabilities netCap, int seqNum, int delay) {
+        if (delay >= 0) {
+            Log.d(TAG, "sending expire msg with seqNum " + seqNum + " and delay " + delay);
+            CallbackHandler handler = getDefaultHandler();
+            Message msg = handler.obtainMessage(EXPIRE_LEGACY_REQUEST, seqNum, 0, netCap);
+            handler.sendMessageDelayed(msg, delay);
+        }
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private boolean removeRequestForFeature(NetworkCapabilities netCap) {
+        final LegacyRequest l;
+        synchronized (sLegacyRequests) {
+            l = sLegacyRequests.remove(netCap);
+        }
+        if (l == null) return false;
+        unregisterNetworkCallback(l.networkCallback);
+        l.clearDnsBinding();
+        return true;
+    }
+
+    private static final SparseIntArray sLegacyTypeToTransport = new SparseIntArray();
+    static {
+        sLegacyTypeToTransport.put(TYPE_MOBILE,       NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_CBS,   NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_DUN,   NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_FOTA,  NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_HIPRI, NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_IMS,   NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_MMS,   NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_SUPL,  NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_WIFI,         NetworkCapabilities.TRANSPORT_WIFI);
+        sLegacyTypeToTransport.put(TYPE_WIFI_P2P,     NetworkCapabilities.TRANSPORT_WIFI);
+        sLegacyTypeToTransport.put(TYPE_BLUETOOTH,    NetworkCapabilities.TRANSPORT_BLUETOOTH);
+        sLegacyTypeToTransport.put(TYPE_ETHERNET,     NetworkCapabilities.TRANSPORT_ETHERNET);
+    }
+
+    private static final SparseIntArray sLegacyTypeToCapability = new SparseIntArray();
+    static {
+        sLegacyTypeToCapability.put(TYPE_MOBILE_CBS,  NetworkCapabilities.NET_CAPABILITY_CBS);
+        sLegacyTypeToCapability.put(TYPE_MOBILE_DUN,  NetworkCapabilities.NET_CAPABILITY_DUN);
+        sLegacyTypeToCapability.put(TYPE_MOBILE_FOTA, NetworkCapabilities.NET_CAPABILITY_FOTA);
+        sLegacyTypeToCapability.put(TYPE_MOBILE_IMS,  NetworkCapabilities.NET_CAPABILITY_IMS);
+        sLegacyTypeToCapability.put(TYPE_MOBILE_MMS,  NetworkCapabilities.NET_CAPABILITY_MMS);
+        sLegacyTypeToCapability.put(TYPE_MOBILE_SUPL, NetworkCapabilities.NET_CAPABILITY_SUPL);
+        sLegacyTypeToCapability.put(TYPE_WIFI_P2P,    NetworkCapabilities.NET_CAPABILITY_WIFI_P2P);
+    }
+
+    /**
+     * Given a legacy type (TYPE_WIFI, ...) returns a NetworkCapabilities
+     * instance suitable for registering a request or callback.  Throws an
+     * IllegalArgumentException if no mapping from the legacy type to
+     * NetworkCapabilities is known.
+     *
+     * @deprecated Types are deprecated. Use {@link NetworkCallback} or {@link NetworkRequest}
+     *     to find the network instead.
+     * @hide
+     */
+    public static NetworkCapabilities networkCapabilitiesForType(int type) {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+
+        // Map from type to transports.
+        final int NOT_FOUND = -1;
+        final int transport = sLegacyTypeToTransport.get(type, NOT_FOUND);
+        if (transport == NOT_FOUND) {
+            throw new IllegalArgumentException("unknown legacy type: " + type);
+        }
+        nc.addTransportType(transport);
+
+        // Map from type to capabilities.
+        nc.addCapability(sLegacyTypeToCapability.get(
+                type, NetworkCapabilities.NET_CAPABILITY_INTERNET));
+        nc.maybeMarkCapabilitiesRestricted();
+        return nc;
+    }
+
+    /** @hide */
+    public static class PacketKeepaliveCallback {
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public PacketKeepaliveCallback() {
+        }
+        /** The requested keepalive was successfully started. */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public void onStarted() {}
+        /** The keepalive was successfully stopped. */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public void onStopped() {}
+        /** An error occurred. */
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public void onError(int error) {}
+    }
+
+    /**
+     * Allows applications to request that the system periodically send specific packets on their
+     * behalf, using hardware offload to save battery power.
+     *
+     * To request that the system send keepalives, call one of the methods that return a
+     * {@link ConnectivityManager.PacketKeepalive} object, such as {@link #startNattKeepalive},
+     * passing in a non-null callback. If the callback is successfully started, the callback's
+     * {@code onStarted} method will be called. If an error occurs, {@code onError} will be called,
+     * specifying one of the {@code ERROR_*} constants in this class.
+     *
+     * To stop an existing keepalive, call {@link PacketKeepalive#stop}. The system will call
+     * {@link PacketKeepaliveCallback#onStopped} if the operation was successful or
+     * {@link PacketKeepaliveCallback#onError} if an error occurred.
+     *
+     * @deprecated Use {@link SocketKeepalive} instead.
+     *
+     * @hide
+     */
+    public class PacketKeepalive {
+
+        private static final String TAG = "PacketKeepalive";
+
+        /** @hide */
+        public static final int SUCCESS = 0;
+
+        /** @hide */
+        public static final int NO_KEEPALIVE = -1;
+
+        /** @hide */
+        public static final int BINDER_DIED = -10;
+
+        /** The specified {@code Network} is not connected. */
+        public static final int ERROR_INVALID_NETWORK = -20;
+        /** The specified IP addresses are invalid. For example, the specified source IP address is
+          * not configured on the specified {@code Network}. */
+        public static final int ERROR_INVALID_IP_ADDRESS = -21;
+        /** The requested port is invalid. */
+        public static final int ERROR_INVALID_PORT = -22;
+        /** The packet length is invalid (e.g., too long). */
+        public static final int ERROR_INVALID_LENGTH = -23;
+        /** The packet transmission interval is invalid (e.g., too short). */
+        public static final int ERROR_INVALID_INTERVAL = -24;
+
+        /** The hardware does not support this request. */
+        public static final int ERROR_HARDWARE_UNSUPPORTED = -30;
+        /** The hardware returned an error. */
+        public static final int ERROR_HARDWARE_ERROR = -31;
+
+        /** The NAT-T destination port for IPsec */
+        public static final int NATT_PORT = 4500;
+
+        /** The minimum interval in seconds between keepalive packet transmissions */
+        public static final int MIN_INTERVAL = 10;
+
+        private final Network mNetwork;
+        private final ISocketKeepaliveCallback mCallback;
+        private final ExecutorService mExecutor;
+
+        private volatile Integer mSlot;
+
+        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+        public void stop() {
+            try {
+                mExecutor.execute(() -> {
+                    try {
+                        if (mSlot != null) {
+                            mService.stopKeepalive(mNetwork, mSlot);
+                        }
+                    } catch (RemoteException e) {
+                        Log.e(TAG, "Error stopping packet keepalive: ", e);
+                        throw e.rethrowFromSystemServer();
+                    }
+                });
+            } catch (RejectedExecutionException e) {
+                // The internal executor has already stopped due to previous event.
+            }
+        }
+
+        private PacketKeepalive(Network network, PacketKeepaliveCallback callback) {
+            Objects.requireNonNull(network, "network cannot be null");
+            Objects.requireNonNull(callback, "callback cannot be null");
+            mNetwork = network;
+            mExecutor = Executors.newSingleThreadExecutor();
+            mCallback = new ISocketKeepaliveCallback.Stub() {
+                @Override
+                public void onStarted(int slot) {
+                    final long token = Binder.clearCallingIdentity();
+                    try {
+                        mExecutor.execute(() -> {
+                            mSlot = slot;
+                            callback.onStarted();
+                        });
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+
+                @Override
+                public void onStopped() {
+                    final long token = Binder.clearCallingIdentity();
+                    try {
+                        mExecutor.execute(() -> {
+                            mSlot = null;
+                            callback.onStopped();
+                        });
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                    mExecutor.shutdown();
+                }
+
+                @Override
+                public void onError(int error) {
+                    final long token = Binder.clearCallingIdentity();
+                    try {
+                        mExecutor.execute(() -> {
+                            mSlot = null;
+                            callback.onError(error);
+                        });
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                    mExecutor.shutdown();
+                }
+
+                @Override
+                public void onDataReceived() {
+                    // PacketKeepalive is only used for Nat-T keepalive and as such does not invoke
+                    // this callback when data is received.
+                }
+            };
+        }
+    }
+
+    /**
+     * Starts an IPsec NAT-T keepalive packet with the specified parameters.
+     *
+     * @deprecated Use {@link #createSocketKeepalive} instead.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public PacketKeepalive startNattKeepalive(
+            Network network, int intervalSeconds, PacketKeepaliveCallback callback,
+            InetAddress srcAddr, int srcPort, InetAddress dstAddr) {
+        final PacketKeepalive k = new PacketKeepalive(network, callback);
+        try {
+            mService.startNattKeepalive(network, intervalSeconds, k.mCallback,
+                    srcAddr.getHostAddress(), srcPort, dstAddr.getHostAddress());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error starting packet keepalive: ", e);
+            throw e.rethrowFromSystemServer();
+        }
+        return k;
+    }
+
+    // Construct an invalid fd.
+    private ParcelFileDescriptor createInvalidFd() {
+        final int invalidFd = -1;
+        return ParcelFileDescriptor.adoptFd(invalidFd);
+    }
+
+    /**
+     * Request that keepalives be started on a IPsec NAT-T socket.
+     *
+     * @param network The {@link Network} the socket is on.
+     * @param socket The socket that needs to be kept alive.
+     * @param source The source address of the {@link UdpEncapsulationSocket}.
+     * @param destination The destination address of the {@link UdpEncapsulationSocket}.
+     * @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 A {@link SocketKeepalive.Callback}. Used for notifications about keepalive
+     *        changes. Must be extended by applications that use this API.
+     *
+     * @return A {@link SocketKeepalive} object that can be used to control the keepalive on the
+     *         given socket.
+     **/
+    public @NonNull SocketKeepalive createSocketKeepalive(@NonNull Network network,
+            @NonNull UdpEncapsulationSocket socket,
+            @NonNull InetAddress source,
+            @NonNull InetAddress destination,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Callback callback) {
+        ParcelFileDescriptor dup;
+        try {
+            // Dup is needed here as the pfd inside the socket is owned by the IpSecService,
+            // which cannot be obtained by the app process.
+            dup = ParcelFileDescriptor.dup(socket.getFileDescriptor());
+        } catch (IOException ignored) {
+            // Construct an invalid fd, so that if the user later calls start(), it will fail with
+            // ERROR_INVALID_SOCKET.
+            dup = createInvalidFd();
+        }
+        return new NattSocketKeepalive(mService, network, dup, socket.getResourceId(), source,
+                destination, executor, callback);
+    }
+
+    /**
+     * Request that keepalives be started on a IPsec NAT-T socket file descriptor. Directly called
+     * by system apps which don't use IpSecService to create {@link UdpEncapsulationSocket}.
+     *
+     * @param network The {@link Network} the socket is on.
+     * @param pfd The {@link ParcelFileDescriptor} that needs to be kept alive. The provided
+     *        {@link ParcelFileDescriptor} must be bound to a port and the keepalives will be sent
+     *        from that port.
+     * @param source The source address of the {@link UdpEncapsulationSocket}.
+     * @param destination The destination address of the {@link UdpEncapsulationSocket}. The
+     *        keepalive packets will always be sent to port 4500 of the given {@code destination}.
+     * @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 A {@link SocketKeepalive.Callback}. Used for notifications about keepalive
+     *        changes. Must be extended by applications that use this API.
+     *
+     * @return A {@link SocketKeepalive} object that can be used to control the keepalive on the
+     *         given socket.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD)
+    public @NonNull SocketKeepalive createNattKeepalive(@NonNull Network network,
+            @NonNull ParcelFileDescriptor pfd,
+            @NonNull InetAddress source,
+            @NonNull InetAddress destination,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Callback callback) {
+        ParcelFileDescriptor dup;
+        try {
+            // TODO: Consider remove unnecessary dup.
+            dup = pfd.dup();
+        } catch (IOException ignored) {
+            // Construct an invalid fd, so that if the user later calls start(), it will fail with
+            // ERROR_INVALID_SOCKET.
+            dup = createInvalidFd();
+        }
+        return new NattSocketKeepalive(mService, network, dup,
+                -1 /* Unused */, source, destination, executor, callback);
+    }
+
+    /**
+     * Request that keepalives be started on a TCP socket.
+     * The socket must be established.
+     *
+     * @param network The {@link Network} the socket is on.
+     * @param socket The socket that needs to be kept alive.
+     * @param executor The executor on which callback will be invoked. This implementation assumes
+     *                 the provided {@link Executor} runs the callbacks in sequence with no
+     *                 concurrency. Failing this, no guarantee of correctness can be made. It is
+     *                 the responsibility of the caller to ensure the executor provides this
+     *                 guarantee. A simple way of creating such an executor is with the standard
+     *                 tool {@code Executors.newSingleThreadExecutor}.
+     * @param callback A {@link SocketKeepalive.Callback}. Used for notifications about keepalive
+     *        changes. Must be extended by applications that use this API.
+     *
+     * @return A {@link SocketKeepalive} object that can be used to control the keepalive on the
+     *         given socket.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD)
+    public @NonNull SocketKeepalive createSocketKeepalive(@NonNull Network network,
+            @NonNull Socket socket,
+            @NonNull Executor executor,
+            @NonNull Callback callback) {
+        ParcelFileDescriptor dup;
+        try {
+            dup = ParcelFileDescriptor.fromSocket(socket);
+        } catch (UncheckedIOException ignored) {
+            // Construct an invalid fd, so that if the user later calls start(), it will fail with
+            // ERROR_INVALID_SOCKET.
+            dup = createInvalidFd();
+        }
+        return new TcpSocketKeepalive(mService, network, dup, executor, callback);
+    }
+
+    /**
+     * Ensure that a network route exists to deliver traffic to the specified
+     * host via the specified network interface. An attempt to add a route that
+     * already exists is ignored, but treated as successful.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param networkType the type of the network over which traffic to the specified
+     * host is to be routed
+     * @param hostAddress the IP address of the host to which the route is desired
+     * @return {@code true} on success, {@code false} on failure
+     *
+     * @deprecated Deprecated in favor of the
+     *             {@link #requestNetwork(NetworkRequest, NetworkCallback)},
+     *             {@link #bindProcessToNetwork} and {@link Network#getSocketFactory} API.
+     *             In {@link VERSION_CODES#M}, and above, this method is unsupported and will
+     *             throw {@code UnsupportedOperationException} if called.
+     * @removed
+     */
+    @Deprecated
+    public boolean requestRouteToHost(int networkType, int hostAddress) {
+        return requestRouteToHostAddress(networkType, NetworkUtils.intToInetAddress(hostAddress));
+    }
+
+    /**
+     * Ensure that a network route exists to deliver traffic to the specified
+     * host via the specified network interface. An attempt to add a route that
+     * already exists is ignored, but treated as successful.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param networkType the type of the network over which traffic to the specified
+     * host is to be routed
+     * @param hostAddress the IP address of the host to which the route is desired
+     * @return {@code true} on success, {@code false} on failure
+     * @hide
+     * @deprecated Deprecated in favor of the {@link #requestNetwork} and
+     *             {@link #bindProcessToNetwork} API.
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    @SystemApi(client = MODULE_LIBRARIES)
+    public boolean requestRouteToHostAddress(int networkType, InetAddress hostAddress) {
+        checkLegacyRoutingApiAccess();
+        try {
+            return mService.requestRouteToHostAddress(networkType, hostAddress.getAddress(),
+                    mContext.getOpPackageName(), getAttributionTag());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @return the context's attribution tag
+     */
+    // TODO: Remove method and replace with direct call once R code is pushed to AOSP
+    private @Nullable String getAttributionTag() {
+        return mContext.getAttributionTag();
+    }
+
+    /**
+     * Returns the value of the setting for background data usage. If false,
+     * applications should not use the network if the application is not in the
+     * foreground. Developers should respect this setting, and check the value
+     * of this before performing any background data operations.
+     * <p>
+     * All applications that have background services that use the network
+     * should listen to {@link #ACTION_BACKGROUND_DATA_SETTING_CHANGED}.
+     * <p>
+     * @deprecated As of {@link VERSION_CODES#ICE_CREAM_SANDWICH}, availability of
+     * background data depends on several combined factors, and this method will
+     * always return {@code true}. Instead, when background data is unavailable,
+     * {@link #getActiveNetworkInfo()} will now appear disconnected.
+     *
+     * @return Whether background data usage is allowed.
+     */
+    @Deprecated
+    public boolean getBackgroundDataSetting() {
+        // assume that background data is allowed; final authority is
+        // NetworkInfo which may be blocked.
+        return true;
+    }
+
+    /**
+     * Sets the value of the setting for background data usage.
+     *
+     * @param allowBackgroundData Whether an application should use data while
+     *            it is in the background.
+     *
+     * @attr ref android.Manifest.permission#CHANGE_BACKGROUND_DATA_SETTING
+     * @see #getBackgroundDataSetting()
+     * @hide
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public void setBackgroundDataSetting(boolean allowBackgroundData) {
+        // ignored
+    }
+
+    /**
+     * @hide
+     * @deprecated Talk to TelephonyManager directly
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public boolean getMobileDataEnabled() {
+        TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+        if (tm != null) {
+            int subId = SubscriptionManager.getDefaultDataSubscriptionId();
+            Log.d("ConnectivityManager", "getMobileDataEnabled()+ subId=" + subId);
+            boolean retVal = tm.createForSubscriptionId(subId).isDataEnabled();
+            Log.d("ConnectivityManager", "getMobileDataEnabled()- subId=" + subId
+                    + " retVal=" + retVal);
+            return retVal;
+        }
+        Log.d("ConnectivityManager", "getMobileDataEnabled()- remote exception retVal=false");
+        return false;
+    }
+
+    /**
+     * Callback for use with {@link ConnectivityManager#addDefaultNetworkActiveListener}
+     * to find out when the system default network has gone in to a high power state.
+     */
+    public interface OnNetworkActiveListener {
+        /**
+         * Called on the main thread of the process to report that the current data network
+         * has become active, and it is now a good time to perform any pending network
+         * operations.  Note that this listener only tells you when the network becomes
+         * active; if at any other time you want to know whether it is active (and thus okay
+         * to initiate network traffic), you can retrieve its instantaneous state with
+         * {@link ConnectivityManager#isDefaultNetworkActive}.
+         */
+        void onNetworkActive();
+    }
+
+    private final ArrayMap<OnNetworkActiveListener, INetworkActivityListener>
+            mNetworkActivityListeners = new ArrayMap<>();
+
+    /**
+     * Start listening to reports when the system's default data network is active, meaning it is
+     * a good time to perform network traffic.  Use {@link #isDefaultNetworkActive()}
+     * to determine the current state of the system's default network after registering the
+     * listener.
+     * <p>
+     * If the process default network has been set with
+     * {@link ConnectivityManager#bindProcessToNetwork} this function will not
+     * reflect the process's default, but the system default.
+     *
+     * @param l The listener to be told when the network is active.
+     */
+    public void addDefaultNetworkActiveListener(final OnNetworkActiveListener l) {
+        INetworkActivityListener rl = new INetworkActivityListener.Stub() {
+            @Override
+            public void onNetworkActive() throws RemoteException {
+                l.onNetworkActive();
+            }
+        };
+
+        try {
+            mService.registerNetworkActivityListener(rl);
+            mNetworkActivityListeners.put(l, rl);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove network active listener previously registered with
+     * {@link #addDefaultNetworkActiveListener}.
+     *
+     * @param l Previously registered listener.
+     */
+    public void removeDefaultNetworkActiveListener(@NonNull OnNetworkActiveListener l) {
+        INetworkActivityListener rl = mNetworkActivityListeners.get(l);
+        if (rl == null) {
+            throw new IllegalArgumentException("Listener was not registered.");
+        }
+        try {
+            mService.registerNetworkActivityListener(rl);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return whether the data network is currently active.  An active network means that
+     * it is currently in a high power state for performing data transmission.  On some
+     * types of networks, it may be expensive to move and stay in such a state, so it is
+     * more power efficient to batch network traffic together when the radio is already in
+     * this state.  This method tells you whether right now is currently a good time to
+     * initiate network traffic, as the network is already active.
+     */
+    public boolean isDefaultNetworkActive() {
+        try {
+            return mService.isDefaultNetworkActive();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * {@hide}
+     */
+    public ConnectivityManager(Context context, IConnectivityManager service) {
+        mContext = Objects.requireNonNull(context, "missing context");
+        mService = Objects.requireNonNull(service, "missing IConnectivityManager");
+        sInstance = this;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public static ConnectivityManager from(Context context) {
+        return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+    }
+
+    /** @hide */
+    public NetworkRequest getDefaultRequest() {
+        try {
+            // This is not racy as the default request is final in ConnectivityService.
+            return mService.getDefaultRequest();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Check if the package is a allowed to write settings. This also accounts that such an access
+     * happened.
+     *
+     * @return {@code true} iff the package is allowed to write settings.
+     */
+    // TODO: Remove method and replace with direct call once R code is pushed to AOSP
+    private static boolean checkAndNoteWriteSettingsOperation(@NonNull Context context, int uid,
+            @NonNull String callingPackage, @Nullable String callingAttributionTag,
+            boolean throwException) {
+        return Settings.checkAndNoteWriteSettingsOperation(context, uid, callingPackage,
+                throwException);
+    }
+
+    /**
+     * @deprecated - use getSystemService. This is a kludge to support static access in certain
+     *               situations where a Context pointer is unavailable.
+     * @hide
+     */
+    @Deprecated
+    static ConnectivityManager getInstanceOrNull() {
+        return sInstance;
+    }
+
+    /**
+     * @deprecated - use getSystemService. This is a kludge to support static access in certain
+     *               situations where a Context pointer is unavailable.
+     * @hide
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    private static ConnectivityManager getInstance() {
+        if (getInstanceOrNull() == null) {
+            throw new IllegalStateException("No ConnectivityManager yet constructed");
+        }
+        return getInstanceOrNull();
+    }
+
+    /**
+     * Get the set of tetherable, available interfaces.  This list is limited by
+     * device configuration and current interface existence.
+     *
+     * @return an array of 0 or more Strings of tetherable interface names.
+     *
+     * @deprecated Use {@link TetheringEventCallback#onTetherableInterfacesChanged(List)} instead.
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    @Deprecated
+    public String[] getTetherableIfaces() {
+        return getTetheringManager().getTetherableIfaces();
+    }
+
+    /**
+     * Get the set of tethered interfaces.
+     *
+     * @return an array of 0 or more String of currently tethered interface names.
+     *
+     * @deprecated Use {@link TetheringEventCallback#onTetherableInterfacesChanged(List)} instead.
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    @Deprecated
+    public String[] getTetheredIfaces() {
+        return getTetheringManager().getTetheredIfaces();
+    }
+
+    /**
+     * Get the set of interface names which attempted to tether but
+     * failed.  Re-attempting to tether may cause them to reset to the Tethered
+     * state.  Alternatively, causing the interface to be destroyed and recreated
+     * may cause them to reset to the available state.
+     * {@link ConnectivityManager#getLastTetherError} can be used to get more
+     * information on the cause of the errors.
+     *
+     * @return an array of 0 or more String indicating the interface names
+     *        which failed to tether.
+     *
+     * @deprecated Use {@link TetheringEventCallback#onError(String, int)} instead.
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    @Deprecated
+    public String[] getTetheringErroredIfaces() {
+        return getTetheringManager().getTetheringErroredIfaces();
+    }
+
+    /**
+     * Get the set of tethered dhcp ranges.
+     *
+     * @deprecated This method is not supported.
+     * TODO: remove this function when all of clients are removed.
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    @Deprecated
+    public String[] getTetheredDhcpRanges() {
+        throw new UnsupportedOperationException("getTetheredDhcpRanges is not supported");
+    }
+
+    /**
+     * Attempt to tether the named interface.  This will setup a dhcp server
+     * on the interface, forward and NAT IP packets and forward DNS requests
+     * to the best active upstream network interface.  Note that if no upstream
+     * IP network interface is available, dhcp will still run and traffic will be
+     * allowed between the tethered devices and this device, though upstream net
+     * access will of course fail until an upstream network interface becomes
+     * active.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * <p>WARNING: New clients should not use this function. The only usages should be in PanService
+     * and WifiStateMachine which need direct access. All other clients should use
+     * {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning
+     * logic.</p>
+     *
+     * @param iface the interface name to tether.
+     * @return error a {@code TETHER_ERROR} value indicating success or failure type
+     * @deprecated Use {@link TetheringManager#startTethering} instead
+     *
+     * {@hide}
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    @Deprecated
+    public int tether(String iface) {
+        return getTetheringManager().tether(iface);
+    }
+
+    /**
+     * Stop tethering the named interface.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * <p>WARNING: New clients should not use this function. The only usages should be in PanService
+     * and WifiStateMachine which need direct access. All other clients should use
+     * {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning
+     * logic.</p>
+     *
+     * @param iface the interface name to untether.
+     * @return error a {@code TETHER_ERROR} value indicating success or failure type
+     *
+     * {@hide}
+     */
+    @UnsupportedAppUsage
+    @Deprecated
+    public int untether(String iface) {
+        return getTetheringManager().untether(iface);
+    }
+
+    /**
+     * Check if the device allows for tethering.  It may be disabled via
+     * {@code ro.tether.denied} system property, Settings.TETHER_SUPPORTED or
+     * due to device configuration.
+     *
+     * <p>If this app does not have permission to use this API, it will always
+     * return false rather than throw an exception.</p>
+     *
+     * <p>If the device has a hotspot provisioning app, the caller is required to hold the
+     * {@link android.Manifest.permission.TETHER_PRIVILEGED} permission.</p>
+     *
+     * <p>Otherwise, this method requires the caller to hold the ability to modify system
+     * settings as determined by {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @return a boolean - {@code true} indicating Tethering is supported.
+     *
+     * @deprecated Use {@link TetheringEventCallback#onTetheringSupported(boolean)} instead.
+     * {@hide}
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {android.Manifest.permission.TETHER_PRIVILEGED,
+            android.Manifest.permission.WRITE_SETTINGS})
+    public boolean isTetheringSupported() {
+        return getTetheringManager().isTetheringSupported();
+    }
+
+    /**
+     * Callback for use with {@link #startTethering} to find out whether tethering succeeded.
+     *
+     * @deprecated Use {@link TetheringManager.StartTetheringCallback} instead.
+     * @hide
+     */
+    @SystemApi
+    @Deprecated
+    public static abstract class OnStartTetheringCallback {
+        /**
+         * Called when tethering has been successfully started.
+         */
+        public void onTetheringStarted() {}
+
+        /**
+         * Called when starting tethering failed.
+         */
+        public void onTetheringFailed() {}
+    }
+
+    /**
+     * Convenient overload for
+     * {@link #startTethering(int, boolean, OnStartTetheringCallback, Handler)} which passes a null
+     * handler to run on the current thread's {@link Looper}.
+     *
+     * @deprecated Use {@link TetheringManager#startTethering} instead.
+     * @hide
+     */
+    @SystemApi
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    public void startTethering(int type, boolean showProvisioningUi,
+            final OnStartTetheringCallback callback) {
+        startTethering(type, showProvisioningUi, callback, null);
+    }
+
+    /**
+     * Runs tether provisioning for the given type if needed and then starts tethering if
+     * the check succeeds. If no carrier provisioning is required for tethering, tethering is
+     * enabled immediately. If provisioning fails, tethering will not be enabled. It also
+     * schedules tether provisioning re-checks if appropriate.
+     *
+     * @param type The type of tethering to start. Must be one of
+     *         {@link ConnectivityManager.TETHERING_WIFI},
+     *         {@link ConnectivityManager.TETHERING_USB}, or
+     *         {@link ConnectivityManager.TETHERING_BLUETOOTH}.
+     * @param showProvisioningUi a boolean indicating to show the provisioning app UI if there
+     *         is one. This should be true the first time this function is called and also any time
+     *         the user can see this UI. It gives users information from their carrier about the
+     *         check failing and how they can sign up for tethering if possible.
+     * @param callback an {@link OnStartTetheringCallback} which will be called to notify the caller
+     *         of the result of trying to tether.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     *
+     * @deprecated Use {@link TetheringManager#startTethering} instead.
+     * @hide
+     */
+    @SystemApi
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    public void startTethering(int type, boolean showProvisioningUi,
+            final OnStartTetheringCallback callback, Handler handler) {
+        Objects.requireNonNull(callback, "OnStartTetheringCallback cannot be null.");
+
+        final Executor executor = new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                if (handler == null) {
+                    command.run();
+                } else {
+                    handler.post(command);
+                }
+            }
+        };
+
+        final StartTetheringCallback tetheringCallback = new StartTetheringCallback() {
+            @Override
+            public void onTetheringStarted() {
+                callback.onTetheringStarted();
+            }
+
+            @Override
+            public void onTetheringFailed(final int error) {
+                callback.onTetheringFailed();
+            }
+        };
+
+        final TetheringRequest request = new TetheringRequest.Builder(type)
+                .setShouldShowEntitlementUi(showProvisioningUi).build();
+
+        getTetheringManager().startTethering(request, executor, tetheringCallback);
+    }
+
+    /**
+     * Stops tethering for the given type. Also cancels any provisioning rechecks for that type if
+     * applicable.
+     *
+     * @param type The type of tethering to stop. Must be one of
+     *         {@link ConnectivityManager.TETHERING_WIFI},
+     *         {@link ConnectivityManager.TETHERING_USB}, or
+     *         {@link ConnectivityManager.TETHERING_BLUETOOTH}.
+     *
+     * @deprecated Use {@link TetheringManager#stopTethering} instead.
+     * @hide
+     */
+    @SystemApi
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    public void stopTethering(int type) {
+        getTetheringManager().stopTethering(type);
+    }
+
+    /**
+     * Callback for use with {@link registerTetheringEventCallback} to find out tethering
+     * upstream status.
+     *
+     * @deprecated Use {@link TetheringManager#OnTetheringEventCallback} instead.
+     * @hide
+     */
+    @SystemApi
+    @Deprecated
+    public abstract static class OnTetheringEventCallback {
+
+        /**
+         * Called when tethering upstream changed. This can be called multiple times and can be
+         * called any time.
+         *
+         * @param network the {@link Network} of tethering upstream. Null means tethering doesn't
+         * have any upstream.
+         */
+        public void onUpstreamChanged(@Nullable Network network) {}
+    }
+
+    @GuardedBy("mTetheringEventCallbacks")
+    private final ArrayMap<OnTetheringEventCallback, TetheringEventCallback>
+            mTetheringEventCallbacks = new ArrayMap<>();
+
+    /**
+     * Start listening to tethering change events. Any new added callback will receive the last
+     * tethering status right away. If callback is registered when tethering has no upstream or
+     * disabled, {@link OnTetheringEventCallback#onUpstreamChanged} will immediately be called
+     * with a null argument. The same callback object cannot be registered twice.
+     *
+     * @param executor the executor on which callback will be invoked.
+     * @param callback the callback to be called when tethering has change events.
+     *
+     * @deprecated Use {@link TetheringManager#registerTetheringEventCallback} instead.
+     * @hide
+     */
+    @SystemApi
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    public void registerTetheringEventCallback(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull final OnTetheringEventCallback callback) {
+        Objects.requireNonNull(callback, "OnTetheringEventCallback cannot be null.");
+
+        final TetheringEventCallback tetherCallback =
+                new TetheringEventCallback() {
+                    @Override
+                    public void onUpstreamChanged(@Nullable Network network) {
+                        callback.onUpstreamChanged(network);
+                    }
+                };
+
+        synchronized (mTetheringEventCallbacks) {
+            mTetheringEventCallbacks.put(callback, tetherCallback);
+            getTetheringManager().registerTetheringEventCallback(executor, tetherCallback);
+        }
+    }
+
+    /**
+     * Remove tethering event callback previously registered with
+     * {@link #registerTetheringEventCallback}.
+     *
+     * @param callback previously registered callback.
+     *
+     * @deprecated Use {@link TetheringManager#unregisterTetheringEventCallback} instead.
+     * @hide
+     */
+    @SystemApi
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    public void unregisterTetheringEventCallback(
+            @NonNull final OnTetheringEventCallback callback) {
+        Objects.requireNonNull(callback, "The callback must be non-null");
+        synchronized (mTetheringEventCallbacks) {
+            final TetheringEventCallback tetherCallback =
+                    mTetheringEventCallbacks.remove(callback);
+            getTetheringManager().unregisterTetheringEventCallback(tetherCallback);
+        }
+    }
+
+
+    /**
+     * Get the list of regular expressions that define any tetherable
+     * USB network interfaces.  If USB tethering is not supported by the
+     * device, this list should be empty.
+     *
+     * @return an array of 0 or more regular expression Strings defining
+     *        what interfaces are considered tetherable usb interfaces.
+     *
+     * @deprecated Use {@link TetheringEventCallback#onTetherableInterfaceRegexpsChanged} instead.
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    @Deprecated
+    public String[] getTetherableUsbRegexs() {
+        return getTetheringManager().getTetherableUsbRegexs();
+    }
+
+    /**
+     * Get the list of regular expressions that define any tetherable
+     * Wifi network interfaces.  If Wifi tethering is not supported by the
+     * device, this list should be empty.
+     *
+     * @return an array of 0 or more regular expression Strings defining
+     *        what interfaces are considered tetherable wifi interfaces.
+     *
+     * @deprecated Use {@link TetheringEventCallback#onTetherableInterfaceRegexpsChanged} instead.
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    @Deprecated
+    public String[] getTetherableWifiRegexs() {
+        return getTetheringManager().getTetherableWifiRegexs();
+    }
+
+    /**
+     * Get the list of regular expressions that define any tetherable
+     * Bluetooth network interfaces.  If Bluetooth tethering is not supported by the
+     * device, this list should be empty.
+     *
+     * @return an array of 0 or more regular expression Strings defining
+     *        what interfaces are considered tetherable bluetooth interfaces.
+     *
+     * @deprecated Use {@link TetheringEventCallback#onTetherableInterfaceRegexpsChanged(
+     *TetheringManager.TetheringInterfaceRegexps)} instead.
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    @Deprecated
+    public String[] getTetherableBluetoothRegexs() {
+        return getTetheringManager().getTetherableBluetoothRegexs();
+    }
+
+    /**
+     * Attempt to both alter the mode of USB and Tethering of USB.  A
+     * utility method to deal with some of the complexity of USB - will
+     * attempt to switch to Rndis and subsequently tether the resulting
+     * interface on {@code true} or turn off tethering and switch off
+     * Rndis on {@code false}.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param enable a boolean - {@code true} to enable tethering
+     * @return error a {@code TETHER_ERROR} value indicating success or failure type
+     * @deprecated Use {@link TetheringManager#startTethering} instead
+     *
+     * {@hide}
+     */
+    @UnsupportedAppUsage
+    @Deprecated
+    public int setUsbTethering(boolean enable) {
+        return getTetheringManager().setUsbTethering(enable);
+    }
+
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_NO_ERROR}.
+     * {@hide}
+     */
+    @SystemApi
+    @Deprecated
+    public static final int TETHER_ERROR_NO_ERROR = 0;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_UNKNOWN_IFACE}.
+     * {@hide}
+     */
+    @Deprecated
+    public static final int TETHER_ERROR_UNKNOWN_IFACE =
+            TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_SERVICE_UNAVAIL}.
+     * {@hide}
+     */
+    @Deprecated
+    public static final int TETHER_ERROR_SERVICE_UNAVAIL =
+            TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_UNSUPPORTED}.
+     * {@hide}
+     */
+    @Deprecated
+    public static final int TETHER_ERROR_UNSUPPORTED = TetheringManager.TETHER_ERROR_UNSUPPORTED;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_UNAVAIL_IFACE}.
+     * {@hide}
+     */
+    @Deprecated
+    public static final int TETHER_ERROR_UNAVAIL_IFACE =
+            TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_INTERNAL_ERROR}.
+     * {@hide}
+     */
+    @Deprecated
+    public static final int TETHER_ERROR_MASTER_ERROR =
+            TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_TETHER_IFACE_ERROR}.
+     * {@hide}
+     */
+    @Deprecated
+    public static final int TETHER_ERROR_TETHER_IFACE_ERROR =
+            TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_UNTETHER_IFACE_ERROR}.
+     * {@hide}
+     */
+    @Deprecated
+    public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR =
+            TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_ENABLE_FORWARDING_ERROR}.
+     * {@hide}
+     */
+    @Deprecated
+    public static final int TETHER_ERROR_ENABLE_NAT_ERROR =
+            TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_DISABLE_FORWARDING_ERROR}.
+     * {@hide}
+     */
+    @Deprecated
+    public static final int TETHER_ERROR_DISABLE_NAT_ERROR =
+            TetheringManager.TETHER_ERROR_DISABLE_FORWARDING_ERROR;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_IFACE_CFG_ERROR}.
+     * {@hide}
+     */
+    @Deprecated
+    public static final int TETHER_ERROR_IFACE_CFG_ERROR =
+            TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_PROVISIONING_FAILED}.
+     * {@hide}
+     */
+    @SystemApi
+    @Deprecated
+    public static final int TETHER_ERROR_PROVISION_FAILED = 11;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_DHCPSERVER_ERROR}.
+     * {@hide}
+     */
+    @Deprecated
+    public static final int TETHER_ERROR_DHCPSERVER_ERROR =
+            TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
+    /**
+     * @deprecated Use {@link TetheringManager#TETHER_ERROR_ENTITLEMENT_UNKNOWN}.
+     * {@hide}
+     */
+    @SystemApi
+    @Deprecated
+    public static final int TETHER_ERROR_ENTITLEMENT_UNKONWN = 13;
+
+    /**
+     * Get a more detailed error code after a Tethering or Untethering
+     * request asynchronously failed.
+     *
+     * @param iface The name of the interface of interest
+     * @return error The error code of the last error tethering or untethering the named
+     *               interface
+     *
+     * @deprecated Use {@link TetheringEventCallback#onError(String, int)} instead.
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    @Deprecated
+    public int getLastTetherError(String iface) {
+        int error = getTetheringManager().getLastTetherError(iface);
+        if (error == TetheringManager.TETHER_ERROR_UNKNOWN_TYPE) {
+            // TETHER_ERROR_UNKNOWN_TYPE was introduced with TetheringManager and has never been
+            // returned by ConnectivityManager. Convert it to the legacy TETHER_ERROR_UNKNOWN_IFACE
+            // instead.
+            error = TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+        }
+        return error;
+    }
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            TETHER_ERROR_NO_ERROR,
+            TETHER_ERROR_PROVISION_FAILED,
+            TETHER_ERROR_ENTITLEMENT_UNKONWN,
+    })
+    public @interface EntitlementResultCode {
+    }
+
+    /**
+     * Callback for use with {@link #getLatestTetheringEntitlementResult} to find out whether
+     * entitlement succeeded.
+     *
+     * @deprecated Use {@link TetheringManager#OnTetheringEntitlementResultListener} instead.
+     * @hide
+     */
+    @SystemApi
+    @Deprecated
+    public interface OnTetheringEntitlementResultListener  {
+        /**
+         * Called to notify entitlement result.
+         *
+         * @param resultCode an int value of entitlement result. It may be one of
+         *         {@link #TETHER_ERROR_NO_ERROR},
+         *         {@link #TETHER_ERROR_PROVISION_FAILED}, or
+         *         {@link #TETHER_ERROR_ENTITLEMENT_UNKONWN}.
+         */
+        void onTetheringEntitlementResult(@EntitlementResultCode int resultCode);
+    }
+
+    /**
+     * Get the last value of the entitlement check on this downstream. If the cached value is
+     * {@link #TETHER_ERROR_NO_ERROR} or showEntitlementUi argument is false, it just return the
+     * cached value. Otherwise, a UI-based entitlement check would be performed. It is not
+     * guaranteed that the UI-based entitlement check will complete in any specific time period
+     * and may in fact never complete. Any successful entitlement check the platform performs for
+     * any reason will update the cached value.
+     *
+     * @param type the downstream type of tethering. Must be one of
+     *         {@link #TETHERING_WIFI},
+     *         {@link #TETHERING_USB}, or
+     *         {@link #TETHERING_BLUETOOTH}.
+     * @param showEntitlementUi a boolean indicating whether to run UI-based entitlement check.
+     * @param executor the executor on which callback will be invoked.
+     * @param listener an {@link OnTetheringEntitlementResultListener} which will be called to
+     *         notify the caller of the result of entitlement check. The listener may be called zero
+     *         or one time.
+     * @deprecated Use {@link TetheringManager#requestLatestTetheringEntitlementResult} instead.
+     * {@hide}
+     */
+    @SystemApi
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    public void getLatestTetheringEntitlementResult(int type, boolean showEntitlementUi,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull final OnTetheringEntitlementResultListener listener) {
+        Objects.requireNonNull(listener, "TetheringEntitlementResultListener cannot be null.");
+        ResultReceiver wrappedListener = new ResultReceiver(null) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                final long token = Binder.clearCallingIdentity();
+                try {
+                    executor.execute(() -> {
+                        listener.onTetheringEntitlementResult(resultCode);
+                    });
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
+            }
+        };
+
+        getTetheringManager().requestLatestTetheringEntitlementResult(type, wrappedListener,
+                    showEntitlementUi);
+    }
+
+    /**
+     * Report network connectivity status.  This is currently used only
+     * to alter status bar UI.
+     * <p>This method requires the caller to hold the permission
+     * {@link android.Manifest.permission#STATUS_BAR}.
+     *
+     * @param networkType The type of network you want to report on
+     * @param percentage The quality of the connection 0 is bad, 100 is good
+     * @deprecated Types are deprecated. Use {@link #reportNetworkConnectivity} instead.
+     * {@hide}
+     */
+    public void reportInetCondition(int networkType, int percentage) {
+        printStackTrace();
+        try {
+            mService.reportInetCondition(networkType, percentage);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Report a problem network to the framework.  This provides a hint to the system
+     * that there might be connectivity problems on this network and may cause
+     * the framework to re-evaluate network connectivity and/or switch to another
+     * network.
+     *
+     * @param network The {@link Network} the application was attempting to use
+     *                or {@code null} to indicate the current default network.
+     * @deprecated Use {@link #reportNetworkConnectivity} which allows reporting both
+     *             working and non-working connectivity.
+     */
+    @Deprecated
+    public void reportBadNetwork(@Nullable Network network) {
+        printStackTrace();
+        try {
+            // One of these will be ignored because it matches system's current state.
+            // The other will trigger the necessary reevaluation.
+            mService.reportNetworkConnectivity(network, true);
+            mService.reportNetworkConnectivity(network, false);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Report to the framework whether a network has working connectivity.
+     * This provides a hint to the system that a particular network is providing
+     * working connectivity or not.  In response the framework may re-evaluate
+     * the network's connectivity and might take further action thereafter.
+     *
+     * @param network The {@link Network} the application was attempting to use
+     *                or {@code null} to indicate the current default network.
+     * @param hasConnectivity {@code true} if the application was able to successfully access the
+     *                        Internet using {@code network} or {@code false} if not.
+     */
+    public void reportNetworkConnectivity(@Nullable Network network, boolean hasConnectivity) {
+        printStackTrace();
+        try {
+            mService.reportNetworkConnectivity(network, hasConnectivity);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set a network-independent global HTTP proxy.
+     *
+     * This sets an HTTP proxy that applies to all networks and overrides any network-specific
+     * proxy. If set, HTTP libraries that are proxy-aware will use this global proxy when
+     * accessing any network, regardless of what the settings for that network are.
+     *
+     * Note that HTTP proxies are by nature typically network-dependent, and setting a global
+     * proxy is likely to break networking on multiple networks. This method is only meant
+     * for device policy clients looking to do general internal filtering or similar use cases.
+     *
+     * {@see #getGlobalProxy}
+     * {@see LinkProperties#getHttpProxy}
+     *
+     * @param p A {@link ProxyInfo} object defining the new global HTTP proxy. Calling this
+     *          method with a {@code null} value will clear the global HTTP proxy.
+     * @hide
+     */
+    // Used by Device Policy Manager to set the global proxy.
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+    public void setGlobalProxy(@Nullable final ProxyInfo p) {
+        try {
+            mService.setGlobalProxy(p);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Retrieve any network-independent global HTTP proxy.
+     *
+     * @return {@link ProxyInfo} for the current global HTTP proxy or {@code null}
+     *        if no global HTTP proxy is set.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @Nullable
+    public ProxyInfo getGlobalProxy() {
+        try {
+            return mService.getGlobalProxy();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Retrieve the global HTTP proxy, or if no global HTTP proxy is set, a
+     * network-specific HTTP proxy.  If {@code network} is null, the
+     * network-specific proxy returned is the proxy of the default active
+     * network.
+     *
+     * @return {@link ProxyInfo} for the current global HTTP proxy, or if no
+     *         global HTTP proxy is set, {@code ProxyInfo} for {@code network},
+     *         or when {@code network} is {@code null},
+     *         the {@code ProxyInfo} for the default active network.  Returns
+     *         {@code null} when no proxy applies or the caller doesn't have
+     *         permission to use {@code network}.
+     * @hide
+     */
+    public ProxyInfo getProxyForNetwork(Network network) {
+        try {
+            return mService.getProxyForNetwork(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the current default HTTP proxy settings.  If a global proxy is set it will be returned,
+     * otherwise if this process is bound to a {@link Network} using
+     * {@link #bindProcessToNetwork} then that {@code Network}'s proxy is returned, otherwise
+     * the default network's proxy is returned.
+     *
+     * @return the {@link ProxyInfo} for the current HTTP proxy, or {@code null} if no
+     *        HTTP proxy is active.
+     */
+    @Nullable
+    public ProxyInfo getDefaultProxy() {
+        return getProxyForNetwork(getBoundNetworkForProcess());
+    }
+
+    /**
+     * Returns true if the hardware supports the given network type
+     * else it returns false.  This doesn't indicate we have coverage
+     * or are authorized onto a network, just whether or not the
+     * hardware supports it.  For example a GSM phone without a SIM
+     * should still return {@code true} for mobile data, but a wifi only
+     * tablet would return {@code false}.
+     *
+     * @param networkType The network type we'd like to check
+     * @return {@code true} if supported, else {@code false}
+     * @deprecated Types are deprecated. Use {@link NetworkCapabilities} instead.
+     * @hide
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public boolean isNetworkSupported(int networkType) {
+        try {
+            return mService.isNetworkSupported(networkType);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns if the currently active data network is metered. A network is
+     * classified as metered when the user is sensitive to heavy data usage on
+     * that connection due to monetary costs, data limitations or
+     * battery/performance issues. You should check this before doing large
+     * data transfers, and warn the user or delay the operation until another
+     * network is available.
+     *
+     * @return {@code true} if large transfers should be avoided, otherwise
+     *        {@code false}.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public boolean isActiveNetworkMetered() {
+        try {
+            return mService.isActiveNetworkMetered();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set sign in error notification to visible or invisible
+     *
+     * @hide
+     * @deprecated Doesn't properly deal with multiple connected networks of the same type.
+     */
+    @Deprecated
+    public void setProvisioningNotificationVisible(boolean visible, int networkType,
+            String action) {
+        try {
+            mService.setProvisioningNotificationVisible(visible, networkType, action);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set the value for enabling/disabling airplane mode
+     *
+     * @param enable whether to enable airplane mode or not
+     *
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_AIRPLANE_MODE,
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD,
+            android.Manifest.permission.NETWORK_STACK})
+    @SystemApi
+    public void setAirplaneMode(boolean enable) {
+        try {
+            mService.setAirplaneMode(enable);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Registers the specified {@link NetworkProvider}.
+     * Each listener must only be registered once. The listener can be unregistered with
+     * {@link #unregisterNetworkProvider}.
+     *
+     * @param provider the provider to register
+     * @return the ID of the provider. This ID must be used by the provider when registering
+     *         {@link android.net.NetworkAgent}s.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_FACTORY})
+    public int registerNetworkProvider(@NonNull NetworkProvider provider) {
+        if (provider.getProviderId() != NetworkProvider.ID_NONE) {
+            throw new IllegalStateException("NetworkProviders can only be registered once");
+        }
+
+        try {
+            int providerId = mService.registerNetworkProvider(provider.getMessenger(),
+                    provider.getName());
+            provider.setProviderId(providerId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        return provider.getProviderId();
+    }
+
+    /**
+     * Unregisters the specified NetworkProvider.
+     *
+     * @param provider the provider to unregister
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_FACTORY})
+    public void unregisterNetworkProvider(@NonNull NetworkProvider provider) {
+        try {
+            mService.unregisterNetworkProvider(provider.getMessenger());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        provider.setProviderId(NetworkProvider.ID_NONE);
+    }
+
+    /**
+     * Register or update a network offer with ConnectivityService.
+     *
+     * ConnectivityService keeps track of offers made by the various providers and matches
+     * them to networking requests made by apps or the system. A callback identifies an offer
+     * uniquely, and later calls with the same callback update the offer. The provider supplies a
+     * score and the capabilities of the network it might be able to bring up ; these act as
+     * filters used by ConnectivityService to only send those requests that can be fulfilled by the
+     * provider.
+     *
+     * The provider is under no obligation to be able to bring up the network it offers at any
+     * given time. Instead, this mechanism is meant to limit requests received by providers
+     * to those they actually have a chance to fulfill, as providers don't have a way to compare
+     * the quality of the network satisfying a given request to their own offer.
+     *
+     * An offer can be updated by calling this again with the same callback object. This is
+     * similar to calling unofferNetwork and offerNetwork again, but will only update the
+     * provider with the changes caused by the changes in the offer.
+     *
+     * @param provider The provider making this offer.
+     * @param score The prospective score of the network.
+     * @param caps The prospective capabilities of the network.
+     * @param callback The callback to call when this offer is needed or unneeded.
+     * @hide exposed via the NetworkProvider class.
+     */
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_FACTORY})
+    public void offerNetwork(@NonNull final int providerId,
+            @NonNull final NetworkScore score, @NonNull final NetworkCapabilities caps,
+            @NonNull final INetworkOfferCallback callback) {
+        try {
+            mService.offerNetwork(providerId,
+                    Objects.requireNonNull(score, "null score"),
+                    Objects.requireNonNull(caps, "null caps"),
+                    Objects.requireNonNull(callback, "null callback"));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Withdraw a network offer made with {@link #offerNetwork}.
+     *
+     * @param callback The callback passed at registration time. This must be the same object
+     *                 that was passed to {@link #offerNetwork}
+     * @hide exposed via the NetworkProvider class.
+     */
+    public void unofferNetwork(@NonNull final INetworkOfferCallback callback) {
+        try {
+            mService.unofferNetwork(Objects.requireNonNull(callback));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+    /** @hide exposed via the NetworkProvider class. */
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_FACTORY})
+    public void declareNetworkRequestUnfulfillable(@NonNull NetworkRequest request) {
+        try {
+            mService.declareNetworkRequestUnfulfillable(request);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     * Register a NetworkAgent with ConnectivityService.
+     * @return Network corresponding to NetworkAgent.
+     */
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_FACTORY})
+    public Network registerNetworkAgent(INetworkAgent na, NetworkInfo ni, LinkProperties lp,
+            NetworkCapabilities nc, @NonNull NetworkScore score, NetworkAgentConfig config,
+            int providerId) {
+        try {
+            return mService.registerNetworkAgent(na, ni, lp, nc, score, config, providerId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Base class for {@code NetworkRequest} callbacks. Used for notifications about network
+     * changes. Should be extended by applications wanting notifications.
+     *
+     * A {@code NetworkCallback} is registered by calling
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback)},
+     * {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)},
+     * or {@link #registerDefaultNetworkCallback(NetworkCallback)}. A {@code NetworkCallback} is
+     * unregistered by calling {@link #unregisterNetworkCallback(NetworkCallback)}.
+     * A {@code NetworkCallback} should be registered at most once at any time.
+     * A {@code NetworkCallback} that has been unregistered can be registered again.
+     */
+    public static class NetworkCallback {
+        /**
+         * No flags associated with this callback.
+         * @hide
+         */
+        public static final int FLAG_NONE = 0;
+        /**
+         * Use this flag to include any location sensitive data in {@link NetworkCapabilities} sent
+         * via {@link #onCapabilitiesChanged(Network, NetworkCapabilities)}.
+         * <p>
+         * These include:
+         * <li> Some transport info instances (retrieved via
+         * {@link NetworkCapabilities#getTransportInfo()}) like {@link android.net.wifi.WifiInfo}
+         * contain location sensitive information.
+         * <li> OwnerUid (retrieved via {@link NetworkCapabilities#getOwnerUid()} is location
+         * sensitive for wifi suggestor apps (i.e using {@link WifiNetworkSuggestion}).</li>
+         * </p>
+         * <p>
+         * Note:
+         * <li> Retrieving this location sensitive information (subject to app's location
+         * permissions) will be noted by system. </li>
+         * <li> Without this flag any {@link NetworkCapabilities} provided via the callback does
+         * not include location sensitive info.
+         * </p>
+         */
+        // Note: Some existing fields which are location sensitive may still be included without
+        // this flag if the app targets SDK < S (to maintain backwards compatibility).
+        public static final int FLAG_INCLUDE_LOCATION_INFO = 1 << 0;
+
+        /** @hide */
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef(flag = true, prefix = "FLAG_", value = {
+                FLAG_NONE,
+                FLAG_INCLUDE_LOCATION_INFO
+        })
+        public @interface Flag { }
+
+        /**
+         * All the valid flags for error checking.
+         */
+        private static final int VALID_FLAGS = FLAG_INCLUDE_LOCATION_INFO;
+
+        public NetworkCallback() {
+            this(FLAG_NONE);
+        }
+
+        public NetworkCallback(@Flag int flags) {
+            if ((flags & VALID_FLAGS) != flags) {
+                throw new IllegalArgumentException("Invalid flags");
+            }
+            mFlags = flags;
+        }
+
+        /**
+         * Called when the framework connects to a new network to evaluate whether it satisfies this
+         * request. If evaluation succeeds, this callback may be followed by an {@link #onAvailable}
+         * callback. There is no guarantee that this new network will satisfy any requests, or that
+         * the network will stay connected for longer than the time necessary to evaluate it.
+         * <p>
+         * Most applications <b>should not</b> act on this callback, and should instead use
+         * {@link #onAvailable}. This callback is intended for use by applications that can assist
+         * the framework in properly evaluating the network &mdash; for example, an application that
+         * can automatically log in to a captive portal without user intervention.
+         *
+         * @param network The {@link Network} of the network that is being evaluated.
+         *
+         * @hide
+         */
+        public void onPreCheck(@NonNull Network network) {}
+
+        /**
+         * Called when the framework connects and has declared a new network ready for use.
+         * This callback may be called more than once if the {@link Network} that is
+         * satisfying the request changes.
+         *
+         * @param network The {@link Network} of the satisfying network.
+         * @param networkCapabilities The {@link NetworkCapabilities} of the satisfying network.
+         * @param linkProperties The {@link LinkProperties} of the satisfying network.
+         * @param blocked Whether access to the {@link Network} is blocked due to system policy.
+         * @hide
+         */
+        public final void onAvailable(@NonNull Network network,
+                @NonNull NetworkCapabilities networkCapabilities,
+                @NonNull LinkProperties linkProperties, @BlockedReason int blocked) {
+            // Internally only this method is called when a new network is available, and
+            // it calls the callback in the same way and order that older versions used
+            // to call so as not to change the behavior.
+            onAvailable(network, networkCapabilities, linkProperties, blocked != 0);
+            onBlockedStatusChanged(network, blocked);
+        }
+
+        /**
+         * Legacy variant of onAvailable that takes a boolean blocked reason.
+         *
+         * This method has never been public API, but it's not final, so there may be apps that
+         * implemented it and rely on it being called. Do our best not to break them.
+         * Note: such apps will also get a second call to onBlockedStatusChanged immediately after
+         * this method is called. There does not seem to be a way to avoid this.
+         * TODO: add a compat check to move apps off this method, and eventually stop calling it.
+         *
+         * @hide
+         */
+        public void onAvailable(@NonNull Network network,
+                @NonNull NetworkCapabilities networkCapabilities,
+                @NonNull LinkProperties linkProperties, boolean blocked) {
+            onAvailable(network);
+            if (!networkCapabilities.hasCapability(
+                    NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)) {
+                onNetworkSuspended(network);
+            }
+            onCapabilitiesChanged(network, networkCapabilities);
+            onLinkPropertiesChanged(network, linkProperties);
+            // No call to onBlockedStatusChanged here. That is done by the caller.
+        }
+
+        /**
+         * Called when the framework connects and has declared a new network ready for use.
+         *
+         * <p>For callbacks registered with {@link #registerNetworkCallback}, multiple networks may
+         * be available at the same time, and onAvailable will be called for each of these as they
+         * appear.
+         *
+         * <p>For callbacks registered with {@link #requestNetwork} and
+         * {@link #registerDefaultNetworkCallback}, this means the network passed as an argument
+         * is the new best network for this request and is now tracked by this callback ; this
+         * callback will no longer receive method calls about other networks that may have been
+         * passed to this method previously. The previously-best network may have disconnected, or
+         * it may still be around and the newly-best network may simply be better.
+         *
+         * <p>Starting with {@link android.os.Build.VERSION_CODES#O}, this will always immediately
+         * be followed by a call to {@link #onCapabilitiesChanged(Network, NetworkCapabilities)}
+         * then by a call to {@link #onLinkPropertiesChanged(Network, LinkProperties)}, and a call
+         * to {@link #onBlockedStatusChanged(Network, boolean)}.
+         *
+         * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or
+         * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in
+         * this callback as this is prone to race conditions (there is no guarantee the objects
+         * returned by these methods will be current). Instead, wait for a call to
+         * {@link #onCapabilitiesChanged(Network, NetworkCapabilities)} and
+         * {@link #onLinkPropertiesChanged(Network, LinkProperties)} whose arguments are guaranteed
+         * to be well-ordered with respect to other callbacks.
+         *
+         * @param network The {@link Network} of the satisfying network.
+         */
+        public void onAvailable(@NonNull Network network) {}
+
+        /**
+         * Called when the network is about to be lost, typically because there are no outstanding
+         * requests left for it. This may be paired with a {@link NetworkCallback#onAvailable} call
+         * with the new replacement network for graceful handover. This method is not guaranteed
+         * to be called before {@link NetworkCallback#onLost} is called, for example in case a
+         * network is suddenly disconnected.
+         *
+         * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or
+         * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in
+         * this callback as this is prone to race conditions ; calling these methods while in a
+         * callback may return an outdated or even a null object.
+         *
+         * @param network The {@link Network} that is about to be lost.
+         * @param maxMsToLive The time in milliseconds the system intends to keep the network
+         *                    connected for graceful handover; note that the network may still
+         *                    suffer a hard loss at any time.
+         */
+        public void onLosing(@NonNull Network network, int maxMsToLive) {}
+
+        /**
+         * Called when a network disconnects or otherwise no longer satisfies this request or
+         * callback.
+         *
+         * <p>If the callback was registered with requestNetwork() or
+         * registerDefaultNetworkCallback(), it will only be invoked against the last network
+         * returned by onAvailable() when that network is lost and no other network satisfies
+         * the criteria of the request.
+         *
+         * <p>If the callback was registered with registerNetworkCallback() it will be called for
+         * each network which no longer satisfies the criteria of the callback.
+         *
+         * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or
+         * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in
+         * this callback as this is prone to race conditions ; calling these methods while in a
+         * callback may return an outdated or even a null object.
+         *
+         * @param network The {@link Network} lost.
+         */
+        public void onLost(@NonNull Network network) {}
+
+        /**
+         * Called if no network is found within the timeout time specified in
+         * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)} call or if the
+         * requested network request cannot be fulfilled (whether or not a timeout was
+         * specified). When this callback is invoked the associated
+         * {@link NetworkRequest} will have already been removed and released, as if
+         * {@link #unregisterNetworkCallback(NetworkCallback)} had been called.
+         */
+        public void onUnavailable() {}
+
+        /**
+         * Called when the network corresponding to this request changes capabilities but still
+         * satisfies the requested criteria.
+         *
+         * <p>Starting with {@link android.os.Build.VERSION_CODES#O} this method is guaranteed
+         * to be called immediately after {@link #onAvailable}.
+         *
+         * <p>Do NOT call {@link #getLinkProperties(Network)} or other synchronous
+         * ConnectivityManager methods in this callback as this is prone to race conditions :
+         * calling these methods while in a callback may return an outdated or even a null object.
+         *
+         * @param network The {@link Network} whose capabilities have changed.
+         * @param networkCapabilities The new {@link NetworkCapabilities} for this
+         *                            network.
+         */
+        public void onCapabilitiesChanged(@NonNull Network network,
+                @NonNull NetworkCapabilities networkCapabilities) {}
+
+        /**
+         * Called when the network corresponding to this request changes {@link LinkProperties}.
+         *
+         * <p>Starting with {@link android.os.Build.VERSION_CODES#O} this method is guaranteed
+         * to be called immediately after {@link #onAvailable}.
+         *
+         * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or other synchronous
+         * ConnectivityManager methods in this callback as this is prone to race conditions :
+         * calling these methods while in a callback may return an outdated or even a null object.
+         *
+         * @param network The {@link Network} whose link properties have changed.
+         * @param linkProperties The new {@link LinkProperties} for this network.
+         */
+        public void onLinkPropertiesChanged(@NonNull Network network,
+                @NonNull LinkProperties linkProperties) {}
+
+        /**
+         * Called when the network the framework connected to for this request suspends data
+         * transmission temporarily.
+         *
+         * <p>This generally means that while the TCP connections are still live temporarily
+         * network data fails to transfer. To give a specific example, this is used on cellular
+         * networks to mask temporary outages when driving through a tunnel, etc. In general this
+         * means read operations on sockets on this network will block once the buffers are
+         * drained, and write operations will block once the buffers are full.
+         *
+         * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or
+         * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in
+         * this callback as this is prone to race conditions (there is no guarantee the objects
+         * returned by these methods will be current).
+         *
+         * @hide
+         */
+        public void onNetworkSuspended(@NonNull Network network) {}
+
+        /**
+         * Called when the network the framework connected to for this request
+         * returns from a {@link NetworkInfo.State#SUSPENDED} state. This should always be
+         * preceded by a matching {@link NetworkCallback#onNetworkSuspended} call.
+
+         * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or
+         * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in
+         * this callback as this is prone to race conditions : calling these methods while in a
+         * callback may return an outdated or even a null object.
+         *
+         * @hide
+         */
+        public void onNetworkResumed(@NonNull Network network) {}
+
+        /**
+         * Called when access to the specified network is blocked or unblocked.
+         *
+         * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or
+         * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in
+         * this callback as this is prone to race conditions : calling these methods while in a
+         * callback may return an outdated or even a null object.
+         *
+         * @param network The {@link Network} whose blocked status has changed.
+         * @param blocked The blocked status of this {@link Network}.
+         */
+        public void onBlockedStatusChanged(@NonNull Network network, boolean blocked) {}
+
+        /**
+         * Called when access to the specified network is blocked or unblocked, or the reason for
+         * access being blocked changes.
+         *
+         * If a NetworkCallback object implements this method,
+         * {@link #onBlockedStatusChanged(Network, boolean)} will not be called.
+         *
+         * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or
+         * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in
+         * this callback as this is prone to race conditions : calling these methods while in a
+         * callback may return an outdated or even a null object.
+         *
+         * @param network The {@link Network} whose blocked status has changed.
+         * @param blocked The blocked status of this {@link Network}.
+         * @hide
+         */
+        @SystemApi(client = MODULE_LIBRARIES)
+        public void onBlockedStatusChanged(@NonNull Network network, @BlockedReason int blocked) {
+            onBlockedStatusChanged(network, blocked != 0);
+        }
+
+        private NetworkRequest networkRequest;
+        private final int mFlags;
+    }
+
+    /**
+     * Constant error codes used by ConnectivityService to communicate about failures and errors
+     * across a Binder boundary.
+     * @hide
+     */
+    public interface Errors {
+        int TOO_MANY_REQUESTS = 1;
+    }
+
+    /** @hide */
+    public static class TooManyRequestsException extends RuntimeException {}
+
+    private static RuntimeException convertServiceException(ServiceSpecificException e) {
+        switch (e.errorCode) {
+            case Errors.TOO_MANY_REQUESTS:
+                return new TooManyRequestsException();
+            default:
+                Log.w(TAG, "Unknown service error code " + e.errorCode);
+                return new RuntimeException(e);
+        }
+    }
+
+    /** @hide */
+    public static final int CALLBACK_PRECHECK            = 1;
+    /** @hide */
+    public static final int CALLBACK_AVAILABLE           = 2;
+    /** @hide arg1 = TTL */
+    public static final int CALLBACK_LOSING              = 3;
+    /** @hide */
+    public static final int CALLBACK_LOST                = 4;
+    /** @hide */
+    public static final int CALLBACK_UNAVAIL             = 5;
+    /** @hide */
+    public static final int CALLBACK_CAP_CHANGED         = 6;
+    /** @hide */
+    public static final int CALLBACK_IP_CHANGED          = 7;
+    /** @hide obj = NetworkCapabilities, arg1 = seq number */
+    private static final int EXPIRE_LEGACY_REQUEST       = 8;
+    /** @hide */
+    public static final int CALLBACK_SUSPENDED           = 9;
+    /** @hide */
+    public static final int CALLBACK_RESUMED             = 10;
+    /** @hide */
+    public static final int CALLBACK_BLK_CHANGED         = 11;
+
+    /** @hide */
+    public static String getCallbackName(int whichCallback) {
+        switch (whichCallback) {
+            case CALLBACK_PRECHECK:     return "CALLBACK_PRECHECK";
+            case CALLBACK_AVAILABLE:    return "CALLBACK_AVAILABLE";
+            case CALLBACK_LOSING:       return "CALLBACK_LOSING";
+            case CALLBACK_LOST:         return "CALLBACK_LOST";
+            case CALLBACK_UNAVAIL:      return "CALLBACK_UNAVAIL";
+            case CALLBACK_CAP_CHANGED:  return "CALLBACK_CAP_CHANGED";
+            case CALLBACK_IP_CHANGED:   return "CALLBACK_IP_CHANGED";
+            case EXPIRE_LEGACY_REQUEST: return "EXPIRE_LEGACY_REQUEST";
+            case CALLBACK_SUSPENDED:    return "CALLBACK_SUSPENDED";
+            case CALLBACK_RESUMED:      return "CALLBACK_RESUMED";
+            case CALLBACK_BLK_CHANGED:  return "CALLBACK_BLK_CHANGED";
+            default:
+                return Integer.toString(whichCallback);
+        }
+    }
+
+    private class CallbackHandler extends Handler {
+        private static final String TAG = "ConnectivityManager.CallbackHandler";
+        private static final boolean DBG = false;
+
+        CallbackHandler(Looper looper) {
+            super(looper);
+        }
+
+        CallbackHandler(Handler handler) {
+            this(Objects.requireNonNull(handler, "Handler cannot be null.").getLooper());
+        }
+
+        @Override
+        public void handleMessage(Message message) {
+            if (message.what == EXPIRE_LEGACY_REQUEST) {
+                expireRequest((NetworkCapabilities) message.obj, message.arg1);
+                return;
+            }
+
+            final NetworkRequest request = getObject(message, NetworkRequest.class);
+            final Network network = getObject(message, Network.class);
+            final NetworkCallback callback;
+            synchronized (sCallbacks) {
+                callback = sCallbacks.get(request);
+                if (callback == null) {
+                    Log.w(TAG,
+                            "callback not found for " + getCallbackName(message.what) + " message");
+                    return;
+                }
+                if (message.what == CALLBACK_UNAVAIL) {
+                    sCallbacks.remove(request);
+                    callback.networkRequest = ALREADY_UNREGISTERED;
+                }
+            }
+            if (DBG) {
+                Log.d(TAG, getCallbackName(message.what) + " for network " + network);
+            }
+
+            switch (message.what) {
+                case CALLBACK_PRECHECK: {
+                    callback.onPreCheck(network);
+                    break;
+                }
+                case CALLBACK_AVAILABLE: {
+                    NetworkCapabilities cap = getObject(message, NetworkCapabilities.class);
+                    LinkProperties lp = getObject(message, LinkProperties.class);
+                    callback.onAvailable(network, cap, lp, message.arg1);
+                    break;
+                }
+                case CALLBACK_LOSING: {
+                    callback.onLosing(network, message.arg1);
+                    break;
+                }
+                case CALLBACK_LOST: {
+                    callback.onLost(network);
+                    break;
+                }
+                case CALLBACK_UNAVAIL: {
+                    callback.onUnavailable();
+                    break;
+                }
+                case CALLBACK_CAP_CHANGED: {
+                    NetworkCapabilities cap = getObject(message, NetworkCapabilities.class);
+                    callback.onCapabilitiesChanged(network, cap);
+                    break;
+                }
+                case CALLBACK_IP_CHANGED: {
+                    LinkProperties lp = getObject(message, LinkProperties.class);
+                    callback.onLinkPropertiesChanged(network, lp);
+                    break;
+                }
+                case CALLBACK_SUSPENDED: {
+                    callback.onNetworkSuspended(network);
+                    break;
+                }
+                case CALLBACK_RESUMED: {
+                    callback.onNetworkResumed(network);
+                    break;
+                }
+                case CALLBACK_BLK_CHANGED: {
+                    callback.onBlockedStatusChanged(network, message.arg1);
+                }
+            }
+        }
+
+        private <T> T getObject(Message msg, Class<T> c) {
+            return (T) msg.getData().getParcelable(c.getSimpleName());
+        }
+    }
+
+    private CallbackHandler getDefaultHandler() {
+        synchronized (sCallbacks) {
+            if (sCallbackHandler == null) {
+                sCallbackHandler = new CallbackHandler(ConnectivityThread.getInstanceLooper());
+            }
+            return sCallbackHandler;
+        }
+    }
+
+    private static final HashMap<NetworkRequest, NetworkCallback> sCallbacks = new HashMap<>();
+    private static CallbackHandler sCallbackHandler;
+
+    private NetworkRequest sendRequestForNetwork(int asUid, NetworkCapabilities need,
+            NetworkCallback callback, int timeoutMs, NetworkRequest.Type reqType, int legacyType,
+            CallbackHandler handler) {
+        printStackTrace();
+        checkCallbackNotNull(callback);
+        if (reqType != TRACK_DEFAULT && reqType != TRACK_SYSTEM_DEFAULT && need == null) {
+            throw new IllegalArgumentException("null NetworkCapabilities");
+        }
+        final NetworkRequest request;
+        final String callingPackageName = mContext.getOpPackageName();
+        try {
+            synchronized(sCallbacks) {
+                if (callback.networkRequest != null
+                        && callback.networkRequest != ALREADY_UNREGISTERED) {
+                    // TODO: throw exception instead and enforce 1:1 mapping of callbacks
+                    // and requests (http://b/20701525).
+                    Log.e(TAG, "NetworkCallback was already registered");
+                }
+                Messenger messenger = new Messenger(handler);
+                Binder binder = new Binder();
+                final int callbackFlags = callback.mFlags;
+                if (reqType == LISTEN) {
+                    request = mService.listenForNetwork(
+                            need, messenger, binder, callbackFlags, callingPackageName,
+                            getAttributionTag());
+                } else {
+                    request = mService.requestNetwork(
+                            asUid, need, reqType.ordinal(), messenger, timeoutMs, binder,
+                            legacyType, callbackFlags, callingPackageName, getAttributionTag());
+                }
+                if (request != null) {
+                    sCallbacks.put(request, callback);
+                }
+                callback.networkRequest = request;
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        } catch (ServiceSpecificException e) {
+            throw convertServiceException(e);
+        }
+        return request;
+    }
+
+    private NetworkRequest sendRequestForNetwork(NetworkCapabilities need, NetworkCallback callback,
+            int timeoutMs, NetworkRequest.Type reqType, int legacyType, CallbackHandler handler) {
+        return sendRequestForNetwork(Process.INVALID_UID, need, callback, timeoutMs, reqType,
+                legacyType, handler);
+    }
+
+    /**
+     * Helper function to request a network with a particular legacy type.
+     *
+     * This API is only for use in internal system code that requests networks with legacy type and
+     * relies on CONNECTIVITY_ACTION broadcasts instead of NetworkCallbacks. New caller should use
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback, Handler)} instead.
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param timeoutMs The time in milliseconds to attempt looking for a suitable network
+     *                  before {@link NetworkCallback#onUnavailable()} is called. The timeout must
+     *                  be a positive value (i.e. >0).
+     * @param legacyType to specify the network type(#TYPE_*).
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+     *                        the callback must not be shared - it uniquely specifies this request.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+    public void requestNetwork(@NonNull NetworkRequest request,
+            int timeoutMs, int legacyType, @NonNull Handler handler,
+            @NonNull NetworkCallback networkCallback) {
+        if (legacyType == TYPE_NONE) {
+            throw new IllegalArgumentException("TYPE_NONE is meaningless legacy type");
+        }
+        CallbackHandler cbHandler = new CallbackHandler(handler);
+        NetworkCapabilities nc = request.networkCapabilities;
+        sendRequestForNetwork(nc, networkCallback, timeoutMs, REQUEST, legacyType, cbHandler);
+    }
+
+    /**
+     * Request a network to satisfy a set of {@link NetworkCapabilities}.
+     *
+     * <p>This method will attempt to find the best network that matches the passed
+     * {@link NetworkRequest}, and to bring up one that does if none currently satisfies the
+     * criteria. The platform will evaluate which network is the best at its own discretion.
+     * Throughput, latency, cost per byte, policy, user preference and other considerations
+     * may be factored in the decision of what is considered the best network.
+     *
+     * <p>As long as this request is outstanding, the platform will try to maintain the best network
+     * matching this request, while always attempting to match the request to a better network if
+     * possible. If a better match is found, the platform will switch this request to the now-best
+     * network and inform the app of the newly best network by invoking
+     * {@link NetworkCallback#onAvailable(Network)} on the provided callback. Note that the platform
+     * will not try to maintain any other network than the best one currently matching the request:
+     * a network not matching any network request may be disconnected at any time.
+     *
+     * <p>For example, an application could use this method to obtain a connected cellular network
+     * even if the device currently has a data connection over Ethernet. This may cause the cellular
+     * radio to consume additional power. Or, an application could inform the system that it wants
+     * a network supporting sending MMSes and have the system let it know about the currently best
+     * MMS-supporting network through the provided {@link NetworkCallback}.
+     *
+     * <p>The status of the request can be followed by listening to the various callbacks described
+     * in {@link NetworkCallback}. The {@link Network} object passed to the callback methods can be
+     * used to direct traffic to the network (although accessing some networks may be subject to
+     * holding specific permissions). Callers will learn about the specific characteristics of the
+     * network through
+     * {@link NetworkCallback#onCapabilitiesChanged(Network, NetworkCapabilities)} and
+     * {@link NetworkCallback#onLinkPropertiesChanged(Network, LinkProperties)}. The methods of the
+     * provided {@link NetworkCallback} will only be invoked due to changes in the best network
+     * matching the request at any given time; therefore when a better network matching the request
+     * becomes available, the {@link NetworkCallback#onAvailable(Network)} method is called
+     * with the new network after which no further updates are given about the previously-best
+     * network, unless it becomes the best again at some later time. All callbacks are invoked
+     * in order on the same thread, which by default is a thread created by the framework running
+     * in the app.
+     * {@see #requestNetwork(NetworkRequest, NetworkCallback, Handler)} to change where the
+     * callbacks are invoked.
+     *
+     * <p>This{@link NetworkRequest} will live until released via
+     * {@link #unregisterNetworkCallback(NetworkCallback)} or the calling application exits, at
+     * which point the system may let go of the network at any time.
+     *
+     * <p>A version of this method which takes a timeout is
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)}, that an app can use to only
+     * wait for a limited amount of time for the network to become unavailable.
+     *
+     * <p>It is presently unsupported to request a network with mutable
+     * {@link NetworkCapabilities} such as
+     * {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED} or
+     * {@link NetworkCapabilities#NET_CAPABILITY_CAPTIVE_PORTAL}
+     * as these {@code NetworkCapabilities} represent states that a particular
+     * network may never attain, and whether a network will attain these states
+     * is unknown prior to bringing up the network so the framework does not
+     * know how to go about satisfying a request with these capabilities.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+     * number of outstanding requests to 100 per app (identified by their UID), shared with
+     * all variants of this method, of {@link #registerNetworkCallback} as well as
+     * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+     * Requesting a network with this method will count toward this limit. If this limit is
+     * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+     * make sure to unregister the callbacks with
+     * {@link #unregisterNetworkCallback(NetworkCallback)}.
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+     *                        the callback must not be shared - it uniquely specifies this request.
+     *                        The callback is invoked on the default internal Handler.
+     * @throws IllegalArgumentException if {@code request} contains invalid network capabilities.
+     * @throws SecurityException if missing the appropriate permissions.
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     */
+    public void requestNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback) {
+        requestNetwork(request, networkCallback, getDefaultHandler());
+    }
+
+    /**
+     * Request a network to satisfy a set of {@link NetworkCapabilities}.
+     *
+     * This method behaves identically to {@link #requestNetwork(NetworkRequest, NetworkCallback)}
+     * but runs all the callbacks on the passed Handler.
+     *
+     * <p>This method has the same permission requirements as
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, is subject to the same limitations,
+     * and throws the same exceptions in the same conditions.
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+     *                        the callback must not be shared - it uniquely specifies this request.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     */
+    public void requestNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler) {
+        CallbackHandler cbHandler = new CallbackHandler(handler);
+        NetworkCapabilities nc = request.networkCapabilities;
+        sendRequestForNetwork(nc, networkCallback, 0, REQUEST, TYPE_NONE, cbHandler);
+    }
+
+    /**
+     * Request a network to satisfy a set of {@link NetworkCapabilities}, limited
+     * by a timeout.
+     *
+     * This function behaves identically to the non-timed-out version
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, but if a suitable network
+     * is not found within the given time (in milliseconds) the
+     * {@link NetworkCallback#onUnavailable()} callback is called. The request can still be
+     * released normally by calling {@link #unregisterNetworkCallback(NetworkCallback)} but does
+     * not have to be released if timed-out (it is automatically released). Unregistering a
+     * request that timed out is not an error.
+     *
+     * <p>Do not use this method to poll for the existence of specific networks (e.g. with a small
+     * timeout) - {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)} is provided
+     * for that purpose. Calling this method will attempt to bring up the requested network.
+     *
+     * <p>This method has the same permission requirements as
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, is subject to the same limitations,
+     * and throws the same exceptions in the same conditions.
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+     *                        the callback must not be shared - it uniquely specifies this request.
+     * @param timeoutMs The time in milliseconds to attempt looking for a suitable network
+     *                  before {@link NetworkCallback#onUnavailable()} is called. The timeout must
+     *                  be a positive value (i.e. >0).
+     */
+    public void requestNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, int timeoutMs) {
+        checkTimeout(timeoutMs);
+        NetworkCapabilities nc = request.networkCapabilities;
+        sendRequestForNetwork(nc, networkCallback, timeoutMs, REQUEST, TYPE_NONE,
+                getDefaultHandler());
+    }
+
+    /**
+     * Request a network to satisfy a set of {@link NetworkCapabilities}, limited
+     * by a timeout.
+     *
+     * This method behaves identically to
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)} but runs all the callbacks
+     * on the passed Handler.
+     *
+     * <p>This method has the same permission requirements as
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, is subject to the same limitations,
+     * and throws the same exceptions in the same conditions.
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+     *                        the callback must not be shared - it uniquely specifies this request.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     * @param timeoutMs The time in milliseconds to attempt looking for a suitable network
+     *                  before {@link NetworkCallback#onUnavailable} is called.
+     */
+    public void requestNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler, int timeoutMs) {
+        checkTimeout(timeoutMs);
+        CallbackHandler cbHandler = new CallbackHandler(handler);
+        NetworkCapabilities nc = request.networkCapabilities;
+        sendRequestForNetwork(nc, networkCallback, timeoutMs, REQUEST, TYPE_NONE, cbHandler);
+    }
+
+    /**
+     * The lookup key for a {@link Network} object included with the intent after
+     * successfully finding a network for the applications request.  Retrieve it with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     * <p>
+     * Note that if you intend to invoke {@link Network#openConnection(java.net.URL)}
+     * then you must get a ConnectivityManager instance before doing so.
+     */
+    public static final String EXTRA_NETWORK = "android.net.extra.NETWORK";
+
+    /**
+     * The lookup key for a {@link NetworkRequest} object included with the intent after
+     * successfully finding a network for the applications request.  Retrieve it with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     */
+    public static final String EXTRA_NETWORK_REQUEST = "android.net.extra.NETWORK_REQUEST";
+
+
+    /**
+     * Request a network to satisfy a set of {@link NetworkCapabilities}.
+     *
+     * This function behaves identically to the version that takes a NetworkCallback, but instead
+     * of {@link NetworkCallback} a {@link PendingIntent} is used.  This means
+     * the request may outlive the calling application and get called back when a suitable
+     * network is found.
+     * <p>
+     * The operation is an Intent broadcast that goes to a broadcast receiver that
+     * you registered with {@link Context#registerReceiver} or through the
+     * &lt;receiver&gt; tag in an AndroidManifest.xml file
+     * <p>
+     * The operation Intent is delivered with two extras, a {@link Network} typed
+     * extra called {@link #EXTRA_NETWORK} and a {@link NetworkRequest}
+     * typed extra called {@link #EXTRA_NETWORK_REQUEST} containing
+     * the original requests parameters.  It is important to create a new,
+     * {@link NetworkCallback} based request before completing the processing of the
+     * Intent to reserve the network or it will be released shortly after the Intent
+     * is processed.
+     * <p>
+     * If there is already a request for this Intent registered (with the equality of
+     * two Intents defined by {@link Intent#filterEquals}), then it will be removed and
+     * replaced by this one, effectively releasing the previous {@link NetworkRequest}.
+     * <p>
+     * The request may be released normally by calling
+     * {@link #releaseNetworkRequest(android.app.PendingIntent)}.
+     * <p>It is presently unsupported to request a network with either
+     * {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED} or
+     * {@link NetworkCapabilities#NET_CAPABILITY_CAPTIVE_PORTAL}
+     * as these {@code NetworkCapabilities} represent states that a particular
+     * network may never attain, and whether a network will attain these states
+     * is unknown prior to bringing up the network so the framework does not
+     * know how to go about satisfying a request with these capabilities.
+     *
+     * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+     * number of outstanding requests to 100 per app (identified by their UID), shared with
+     * all variants of this method, of {@link #registerNetworkCallback} as well as
+     * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+     * Requesting a network with this method will count toward this limit. If this limit is
+     * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+     * make sure to unregister the callbacks with {@link #unregisterNetworkCallback(PendingIntent)}
+     * or {@link #releaseNetworkRequest(PendingIntent)}.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param operation Action to perform when the network is available (corresponds
+     *                  to the {@link NetworkCallback#onAvailable} call.  Typically
+     *                  comes from {@link PendingIntent#getBroadcast}. Cannot be null.
+     * @throws IllegalArgumentException if {@code request} contains invalid network capabilities.
+     * @throws SecurityException if missing the appropriate permissions.
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     */
+    public void requestNetwork(@NonNull NetworkRequest request,
+            @NonNull PendingIntent operation) {
+        printStackTrace();
+        checkPendingIntentNotNull(operation);
+        try {
+            mService.pendingRequestForNetwork(
+                    request.networkCapabilities, operation, mContext.getOpPackageName(),
+                    getAttributionTag());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        } catch (ServiceSpecificException e) {
+            throw convertServiceException(e);
+        }
+    }
+
+    /**
+     * Removes a request made via {@link #requestNetwork(NetworkRequest, android.app.PendingIntent)}
+     * <p>
+     * This method has the same behavior as
+     * {@link #unregisterNetworkCallback(android.app.PendingIntent)} with respect to
+     * releasing network resources and disconnecting.
+     *
+     * @param operation A PendingIntent equal (as defined by {@link Intent#filterEquals}) to the
+     *                  PendingIntent passed to
+     *                  {@link #requestNetwork(NetworkRequest, android.app.PendingIntent)} with the
+     *                  corresponding NetworkRequest you'd like to remove. Cannot be null.
+     */
+    public void releaseNetworkRequest(@NonNull PendingIntent operation) {
+        printStackTrace();
+        checkPendingIntentNotNull(operation);
+        try {
+            mService.releasePendingNetworkRequest(operation);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private static void checkPendingIntentNotNull(PendingIntent intent) {
+        Objects.requireNonNull(intent, "PendingIntent cannot be null.");
+    }
+
+    private static void checkCallbackNotNull(NetworkCallback callback) {
+        Objects.requireNonNull(callback, "null NetworkCallback");
+    }
+
+    private static void checkTimeout(int timeoutMs) {
+        if (timeoutMs <= 0) {
+            throw new IllegalArgumentException("timeoutMs must be strictly positive.");
+        }
+    }
+
+    /**
+     * Registers to receive notifications about all networks which satisfy the given
+     * {@link NetworkRequest}.  The callbacks will continue to be called until
+     * either the application exits or {@link #unregisterNetworkCallback(NetworkCallback)} is
+     * called.
+     *
+     * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+     * number of outstanding requests to 100 per app (identified by their UID), shared with
+     * all variants of this method, of {@link #requestNetwork} as well as
+     * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+     * Requesting a network with this method will count toward this limit. If this limit is
+     * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+     * make sure to unregister the callbacks with
+     * {@link #unregisterNetworkCallback(NetworkCallback)}.
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} that the system will call as suitable
+     *                        networks change state.
+     *                        The callback is invoked on the default internal Handler.
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public void registerNetworkCallback(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback) {
+        registerNetworkCallback(request, networkCallback, getDefaultHandler());
+    }
+
+    /**
+     * Registers to receive notifications about all networks which satisfy the given
+     * {@link NetworkRequest}.  The callbacks will continue to be called until
+     * either the application exits or {@link #unregisterNetworkCallback(NetworkCallback)} is
+     * called.
+     *
+     * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+     * number of outstanding requests to 100 per app (identified by their UID), shared with
+     * all variants of this method, of {@link #requestNetwork} as well as
+     * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+     * Requesting a network with this method will count toward this limit. If this limit is
+     * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+     * make sure to unregister the callbacks with
+     * {@link #unregisterNetworkCallback(NetworkCallback)}.
+     *
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} that the system will call as suitable
+     *                        networks change state.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public void registerNetworkCallback(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler) {
+        CallbackHandler cbHandler = new CallbackHandler(handler);
+        NetworkCapabilities nc = request.networkCapabilities;
+        sendRequestForNetwork(nc, networkCallback, 0, LISTEN, TYPE_NONE, cbHandler);
+    }
+
+    /**
+     * Registers a PendingIntent to be sent when a network is available which satisfies the given
+     * {@link NetworkRequest}.
+     *
+     * This function behaves identically to the version that takes a NetworkCallback, but instead
+     * of {@link NetworkCallback} a {@link PendingIntent} is used.  This means
+     * the request may outlive the calling application and get called back when a suitable
+     * network is found.
+     * <p>
+     * The operation is an Intent broadcast that goes to a broadcast receiver that
+     * you registered with {@link Context#registerReceiver} or through the
+     * &lt;receiver&gt; tag in an AndroidManifest.xml file
+     * <p>
+     * The operation Intent is delivered with two extras, a {@link Network} typed
+     * extra called {@link #EXTRA_NETWORK} and a {@link NetworkRequest}
+     * typed extra called {@link #EXTRA_NETWORK_REQUEST} containing
+     * the original requests parameters.
+     * <p>
+     * If there is already a request for this Intent registered (with the equality of
+     * two Intents defined by {@link Intent#filterEquals}), then it will be removed and
+     * replaced by this one, effectively releasing the previous {@link NetworkRequest}.
+     * <p>
+     * The request may be released normally by calling
+     * {@link #unregisterNetworkCallback(android.app.PendingIntent)}.
+     *
+     * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+     * number of outstanding requests to 100 per app (identified by their UID), shared with
+     * all variants of this method, of {@link #requestNetwork} as well as
+     * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+     * Requesting a network with this method will count toward this limit. If this limit is
+     * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+     * make sure to unregister the callbacks with {@link #unregisterNetworkCallback(PendingIntent)}
+     * or {@link #releaseNetworkRequest(PendingIntent)}.
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param operation Action to perform when the network is available (corresponds
+     *                  to the {@link NetworkCallback#onAvailable} call.  Typically
+     *                  comes from {@link PendingIntent#getBroadcast}. Cannot be null.
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public void registerNetworkCallback(@NonNull NetworkRequest request,
+            @NonNull PendingIntent operation) {
+        printStackTrace();
+        checkPendingIntentNotNull(operation);
+        try {
+            mService.pendingListenForNetwork(
+                    request.networkCapabilities, operation, mContext.getOpPackageName(),
+                    getAttributionTag());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        } catch (ServiceSpecificException e) {
+            throw convertServiceException(e);
+        }
+    }
+
+    /**
+     * Registers to receive notifications about changes in the application's default network. This
+     * may be a physical network or a virtual network, such as a VPN that applies to the
+     * application. The callbacks will continue to be called until either the application exits or
+     * {@link #unregisterNetworkCallback(NetworkCallback)} is called.
+     *
+     * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+     * number of outstanding requests to 100 per app (identified by their UID), shared with
+     * all variants of this method, of {@link #requestNetwork} as well as
+     * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+     * Requesting a network with this method will count toward this limit. If this limit is
+     * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+     * make sure to unregister the callbacks with
+     * {@link #unregisterNetworkCallback(NetworkCallback)}.
+     *
+     * @param networkCallback The {@link NetworkCallback} that the system will call as the
+     *                        application's default network changes.
+     *                        The callback is invoked on the default internal Handler.
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public void registerDefaultNetworkCallback(@NonNull NetworkCallback networkCallback) {
+        registerDefaultNetworkCallback(networkCallback, getDefaultHandler());
+    }
+
+    /**
+     * Registers to receive notifications about changes in the application's default network. This
+     * may be a physical network or a virtual network, such as a VPN that applies to the
+     * application. The callbacks will continue to be called until either the application exits or
+     * {@link #unregisterNetworkCallback(NetworkCallback)} is called.
+     *
+     * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+     * number of outstanding requests to 100 per app (identified by their UID), shared with
+     * all variants of this method, of {@link #requestNetwork} as well as
+     * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+     * Requesting a network with this method will count toward this limit. If this limit is
+     * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+     * make sure to unregister the callbacks with
+     * {@link #unregisterNetworkCallback(NetworkCallback)}.
+     *
+     * @param networkCallback The {@link NetworkCallback} that the system will call as the
+     *                        application's default network changes.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public void registerDefaultNetworkCallback(@NonNull NetworkCallback networkCallback,
+            @NonNull Handler handler) {
+        registerDefaultNetworkCallbackForUid(Process.INVALID_UID, networkCallback, handler);
+    }
+
+    /**
+     * Registers to receive notifications about changes in the default network for the specified
+     * UID. This may be a physical network or a virtual network, such as a VPN that applies to the
+     * UID. The callbacks will continue to be called until either the application exits or
+     * {@link #unregisterNetworkCallback(NetworkCallback)} is called.
+     *
+     * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+     * number of outstanding requests to 100 per app (identified by their UID), shared with
+     * all variants of this method, of {@link #requestNetwork} as well as
+     * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+     * Requesting a network with this method will count toward this limit. If this limit is
+     * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+     * make sure to unregister the callbacks with
+     * {@link #unregisterNetworkCallback(NetworkCallback)}.
+     *
+     * @param uid the UID for which to track default network changes.
+     * @param networkCallback The {@link NetworkCallback} that the system will call as the
+     *                        UID's default network changes.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @SuppressLint({"ExecutorRegistration", "PairedRegistration"})
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_SETTINGS})
+    public void registerDefaultNetworkCallbackForUid(int uid,
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler) {
+        CallbackHandler cbHandler = new CallbackHandler(handler);
+        sendRequestForNetwork(uid, null /* need */, networkCallback, 0 /* timeoutMs */,
+                TRACK_DEFAULT, TYPE_NONE, cbHandler);
+    }
+
+    /**
+     * Registers to receive notifications about changes in the system default network. The callbacks
+     * will continue to be called until either the application exits or
+     * {@link #unregisterNetworkCallback(NetworkCallback)} is called.
+     *
+     * This method should not be used to determine networking state seen by applications, because in
+     * many cases, most or even all application traffic may not use the default network directly,
+     * and traffic from different applications may go on different networks by default. As an
+     * example, if a VPN is connected, traffic from all applications might be sent through the VPN
+     * and not onto the system default network. Applications or system components desiring to do
+     * determine network state as seen by applications should use other methods such as
+     * {@link #registerDefaultNetworkCallback(NetworkCallback, Handler)}.
+     *
+     * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+     * number of outstanding requests to 100 per app (identified by their UID), shared with
+     * all variants of this method, of {@link #requestNetwork} as well as
+     * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+     * Requesting a network with this method will count toward this limit. If this limit is
+     * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+     * make sure to unregister the callbacks with
+     * {@link #unregisterNetworkCallback(NetworkCallback)}.
+     *
+     * @param networkCallback The {@link NetworkCallback} that the system will call as the
+     *                        system default network changes.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @SuppressLint({"ExecutorRegistration", "PairedRegistration"})
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_SETTINGS})
+    public void registerSystemDefaultNetworkCallback(@NonNull NetworkCallback networkCallback,
+            @NonNull Handler handler) {
+        CallbackHandler cbHandler = new CallbackHandler(handler);
+        sendRequestForNetwork(null /* NetworkCapabilities need */, networkCallback, 0,
+                TRACK_SYSTEM_DEFAULT, TYPE_NONE, cbHandler);
+    }
+
+    /**
+     * Registers to receive notifications about the best matching network which satisfy the given
+     * {@link NetworkRequest}.  The callbacks will continue to be called until
+     * either the application exits or {@link #unregisterNetworkCallback(NetworkCallback)} is
+     * called.
+     *
+     * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+     * number of outstanding requests to 100 per app (identified by their UID), shared with
+     * {@link #registerNetworkCallback} and its variants and {@link #requestNetwork} as well as
+     * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+     * Requesting a network with this method will count toward this limit. If this limit is
+     * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+     * make sure to unregister the callbacks with
+     * {@link #unregisterNetworkCallback(NetworkCallback)}.
+     *
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} that the system will call as suitable
+     *                        networks change state.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     */
+    @SuppressLint("ExecutorRegistration")
+    public void registerBestMatchingNetworkCallback(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler) {
+        final NetworkCapabilities nc = request.networkCapabilities;
+        final CallbackHandler cbHandler = new CallbackHandler(handler);
+        sendRequestForNetwork(nc, networkCallback, 0, LISTEN_FOR_BEST, TYPE_NONE, cbHandler);
+    }
+
+    /**
+     * Requests bandwidth update for a given {@link Network} and returns whether the update request
+     * is accepted by ConnectivityService. Once accepted, ConnectivityService will poll underlying
+     * network connection for updated bandwidth information. The caller will be notified via
+     * {@link ConnectivityManager.NetworkCallback} if there is an update. Notice that this
+     * method assumes that the caller has previously called
+     * {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)} to listen for network
+     * changes.
+     *
+     * @param network {@link Network} specifying which network you're interested.
+     * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid.
+     */
+    public boolean requestBandwidthUpdate(@NonNull Network network) {
+        try {
+            return mService.requestBandwidthUpdate(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Unregisters a {@code NetworkCallback} and possibly releases networks originating from
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback)} and
+     * {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)} calls.
+     * If the given {@code NetworkCallback} had previously been used with
+     * {@code #requestNetwork}, any networks that had been connected to only to satisfy that request
+     * will be disconnected.
+     *
+     * Notifications that would have triggered that {@code NetworkCallback} will immediately stop
+     * triggering it as soon as this call returns.
+     *
+     * @param networkCallback The {@link NetworkCallback} used when making the request.
+     */
+    public void unregisterNetworkCallback(@NonNull NetworkCallback networkCallback) {
+        printStackTrace();
+        checkCallbackNotNull(networkCallback);
+        final List<NetworkRequest> reqs = new ArrayList<>();
+        // Find all requests associated to this callback and stop callback triggers immediately.
+        // Callback is reusable immediately. http://b/20701525, http://b/35921499.
+        synchronized (sCallbacks) {
+            if (networkCallback.networkRequest == null) {
+                throw new IllegalArgumentException("NetworkCallback was not registered");
+            }
+            if (networkCallback.networkRequest == ALREADY_UNREGISTERED) {
+                Log.d(TAG, "NetworkCallback was already unregistered");
+                return;
+            }
+            for (Map.Entry<NetworkRequest, NetworkCallback> e : sCallbacks.entrySet()) {
+                if (e.getValue() == networkCallback) {
+                    reqs.add(e.getKey());
+                }
+            }
+            // TODO: throw exception if callback was registered more than once (http://b/20701525).
+            for (NetworkRequest r : reqs) {
+                try {
+                    mService.releaseNetworkRequest(r);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+                // Only remove mapping if rpc was successful.
+                sCallbacks.remove(r);
+            }
+            networkCallback.networkRequest = ALREADY_UNREGISTERED;
+        }
+    }
+
+    /**
+     * Unregisters a callback previously registered via
+     * {@link #registerNetworkCallback(NetworkRequest, android.app.PendingIntent)}.
+     *
+     * @param operation A PendingIntent equal (as defined by {@link Intent#filterEquals}) to the
+     *                  PendingIntent passed to
+     *                  {@link #registerNetworkCallback(NetworkRequest, android.app.PendingIntent)}.
+     *                  Cannot be null.
+     */
+    public void unregisterNetworkCallback(@NonNull PendingIntent operation) {
+        releaseNetworkRequest(operation);
+    }
+
+    /**
+     * Informs the system whether it should switch to {@code network} regardless of whether it is
+     * validated or not. If {@code accept} is true, and the network was explicitly selected by the
+     * user (e.g., by selecting a Wi-Fi network in the Settings app), then the network will become
+     * the system default network regardless of any other network that's currently connected. If
+     * {@code always} is true, then the choice is remembered, so that the next time the user
+     * connects to this network, the system will switch to it.
+     *
+     * @param network The network to accept.
+     * @param accept Whether to accept the network even if unvalidated.
+     * @param always Whether to remember this choice in the future.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
+    public void setAcceptUnvalidated(@NonNull Network network, boolean accept, boolean always) {
+        try {
+            mService.setAcceptUnvalidated(network, accept, always);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Informs the system whether it should consider the network as validated even if it only has
+     * partial connectivity. If {@code accept} is true, then the network will be considered as
+     * validated even if connectivity is only partial. If {@code always} is true, then the choice
+     * is remembered, so that the next time the user connects to this network, the system will
+     * switch to it.
+     *
+     * @param network The network to accept.
+     * @param accept Whether to consider the network as validated even if it has partial
+     *               connectivity.
+     * @param always Whether to remember this choice in the future.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
+    public void setAcceptPartialConnectivity(@NonNull Network network, boolean accept,
+            boolean always) {
+        try {
+            mService.setAcceptPartialConnectivity(network, accept, always);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Informs the system to penalize {@code network}'s score when it becomes unvalidated. This is
+     * only meaningful if the system is configured not to penalize such networks, e.g., if the
+     * {@code config_networkAvoidBadWifi} configuration variable is set to 0 and the {@code
+     * NETWORK_AVOID_BAD_WIFI setting is unset}.
+     *
+     * @param network The network to accept.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
+    public void setAvoidUnvalidated(@NonNull Network network) {
+        try {
+            mService.setAvoidUnvalidated(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Requests that the system open the captive portal app on the specified network.
+     *
+     * <p>This is to be used on networks where a captive portal was detected, as per
+     * {@link NetworkCapabilities#NET_CAPABILITY_CAPTIVE_PORTAL}.
+     *
+     * @param network The network to log into.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+    })
+    public void startCaptivePortalApp(@NonNull Network network) {
+        try {
+            mService.startCaptivePortalApp(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Requests that the system open the captive portal app with the specified extras.
+     *
+     * <p>This endpoint is exclusively for use by the NetworkStack and is protected by the
+     * corresponding permission.
+     * @param network Network on which the captive portal was detected.
+     * @param appExtras Extras to include in the app start intent.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+    public void startCaptivePortalApp(@NonNull Network network, @NonNull Bundle appExtras) {
+        try {
+            mService.startCaptivePortalAppInternal(network, appExtras);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Determine whether the device is configured to avoid bad wifi.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK})
+    public boolean shouldAvoidBadWifi() {
+        try {
+            return mService.shouldAvoidBadWifi();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * It is acceptable to briefly use multipath data to provide seamless connectivity for
+     * time-sensitive user-facing operations when the system default network is temporarily
+     * unresponsive. The amount of data should be limited (less than one megabyte for every call to
+     * this method), and the operation should be infrequent to ensure that data usage is limited.
+     *
+     * An example of such an operation might be a time-sensitive foreground activity, such as a
+     * voice command, that the user is performing while walking out of range of a Wi-Fi network.
+     */
+    public static final int MULTIPATH_PREFERENCE_HANDOVER = 1 << 0;
+
+    /**
+     * It is acceptable to use small amounts of multipath data on an ongoing basis to provide
+     * a backup channel for traffic that is primarily going over another network.
+     *
+     * An example might be maintaining backup connections to peers or servers for the purpose of
+     * fast fallback if the default network is temporarily unresponsive or disconnects. The traffic
+     * on backup paths should be negligible compared to the traffic on the main path.
+     */
+    public static final int MULTIPATH_PREFERENCE_RELIABILITY = 1 << 1;
+
+    /**
+     * It is acceptable to use metered data to improve network latency and performance.
+     */
+    public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 1 << 2;
+
+    /**
+     * Return value to use for unmetered networks. On such networks we currently set all the flags
+     * to true.
+     * @hide
+     */
+    public static final int MULTIPATH_PREFERENCE_UNMETERED =
+            MULTIPATH_PREFERENCE_HANDOVER |
+            MULTIPATH_PREFERENCE_RELIABILITY |
+            MULTIPATH_PREFERENCE_PERFORMANCE;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, value = {
+            MULTIPATH_PREFERENCE_HANDOVER,
+            MULTIPATH_PREFERENCE_RELIABILITY,
+            MULTIPATH_PREFERENCE_PERFORMANCE,
+    })
+    public @interface MultipathPreference {
+    }
+
+    /**
+     * Provides a hint to the calling application on whether it is desirable to use the
+     * multinetwork APIs (e.g., {@link Network#openConnection}, {@link Network#bindSocket}, etc.)
+     * for multipath data transfer on this network when it is not the system default network.
+     * Applications desiring to use multipath network protocols should call this method before
+     * each such operation.
+     *
+     * @param network The network on which the application desires to use multipath data.
+     *                If {@code null}, this method will return the a preference that will generally
+     *                apply to metered networks.
+     * @return a bitwise OR of zero or more of the  {@code MULTIPATH_PREFERENCE_*} constants.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public @MultipathPreference int getMultipathPreference(@Nullable Network network) {
+        try {
+            return mService.getMultipathPreference(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Resets all connectivity manager settings back to factory defaults.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
+    public void factoryReset() {
+        try {
+            mService.factoryReset();
+            getTetheringManager().stopAllTethering();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Binds the current process to {@code network}.  All Sockets created in the future
+     * (and not explicitly bound via a bound SocketFactory from
+     * {@link Network#getSocketFactory() Network.getSocketFactory()}) will be bound to
+     * {@code network}.  All host name resolutions will be limited to {@code network} as well.
+     * Note that if {@code network} ever disconnects, all Sockets created in this way will cease to
+     * work and all host name resolutions will fail.  This is by design so an application doesn't
+     * accidentally use Sockets it thinks are still bound to a particular {@link Network}.
+     * To clear binding pass {@code null} for {@code network}.  Using individually bound
+     * Sockets created by Network.getSocketFactory().createSocket() and
+     * performing network-specific host name resolutions via
+     * {@link Network#getAllByName Network.getAllByName} is preferred to calling
+     * {@code bindProcessToNetwork}.
+     *
+     * @param network The {@link Network} to bind the current process to, or {@code null} to clear
+     *                the current binding.
+     * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid.
+     */
+    public boolean bindProcessToNetwork(@Nullable Network network) {
+        // Forcing callers to call through non-static function ensures ConnectivityManager
+        // instantiated.
+        return setProcessDefaultNetwork(network);
+    }
+
+    /**
+     * Binds the current process to {@code network}.  All Sockets created in the future
+     * (and not explicitly bound via a bound SocketFactory from
+     * {@link Network#getSocketFactory() Network.getSocketFactory()}) will be bound to
+     * {@code network}.  All host name resolutions will be limited to {@code network} as well.
+     * Note that if {@code network} ever disconnects, all Sockets created in this way will cease to
+     * work and all host name resolutions will fail.  This is by design so an application doesn't
+     * accidentally use Sockets it thinks are still bound to a particular {@link Network}.
+     * To clear binding pass {@code null} for {@code network}.  Using individually bound
+     * Sockets created by Network.getSocketFactory().createSocket() and
+     * performing network-specific host name resolutions via
+     * {@link Network#getAllByName Network.getAllByName} is preferred to calling
+     * {@code setProcessDefaultNetwork}.
+     *
+     * @param network The {@link Network} to bind the current process to, or {@code null} to clear
+     *                the current binding.
+     * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid.
+     * @deprecated This function can throw {@link IllegalStateException}.  Use
+     *             {@link #bindProcessToNetwork} instead.  {@code bindProcessToNetwork}
+     *             is a direct replacement.
+     */
+    @Deprecated
+    public static boolean setProcessDefaultNetwork(@Nullable Network network) {
+        int netId = (network == null) ? NETID_UNSET : network.netId;
+        boolean isSameNetId = (netId == NetworkUtils.getBoundNetworkForProcess());
+
+        if (netId != NETID_UNSET) {
+            netId = network.getNetIdForResolv();
+        }
+
+        if (!NetworkUtils.bindProcessToNetwork(netId)) {
+            return false;
+        }
+
+        if (!isSameNetId) {
+            // Set HTTP proxy system properties to match network.
+            // TODO: Deprecate this static method and replace it with a non-static version.
+            try {
+                Proxy.setHttpProxyConfiguration(getInstance().getDefaultProxy());
+            } catch (SecurityException e) {
+                // The process doesn't have ACCESS_NETWORK_STATE, so we can't fetch the proxy.
+                Log.e(TAG, "Can't set proxy properties", e);
+            }
+            // Must flush DNS cache as new network may have different DNS resolutions.
+            InetAddressCompat.clearDnsCache();
+            // Must flush socket pool as idle sockets will be bound to previous network and may
+            // cause subsequent fetches to be performed on old network.
+            NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the {@link Network} currently bound to this process via
+     * {@link #bindProcessToNetwork}, or {@code null} if no {@link Network} is explicitly bound.
+     *
+     * @return {@code Network} to which this process is bound, or {@code null}.
+     */
+    @Nullable
+    public Network getBoundNetworkForProcess() {
+        // Forcing callers to call thru non-static function ensures ConnectivityManager
+        // instantiated.
+        return getProcessDefaultNetwork();
+    }
+
+    /**
+     * Returns the {@link Network} currently bound to this process via
+     * {@link #bindProcessToNetwork}, or {@code null} if no {@link Network} is explicitly bound.
+     *
+     * @return {@code Network} to which this process is bound, or {@code null}.
+     * @deprecated Using this function can lead to other functions throwing
+     *             {@link IllegalStateException}.  Use {@link #getBoundNetworkForProcess} instead.
+     *             {@code getBoundNetworkForProcess} is a direct replacement.
+     */
+    @Deprecated
+    @Nullable
+    public static Network getProcessDefaultNetwork() {
+        int netId = NetworkUtils.getBoundNetworkForProcess();
+        if (netId == NETID_UNSET) return null;
+        return new Network(netId);
+    }
+
+    private void unsupportedStartingFrom(int version) {
+        if (Process.myUid() == Process.SYSTEM_UID) {
+            // The getApplicationInfo() call we make below is not supported in system context. Let
+            // the call through here, and rely on the fact that ConnectivityService will refuse to
+            // allow the system to use these APIs anyway.
+            return;
+        }
+
+        if (mContext.getApplicationInfo().targetSdkVersion >= version) {
+            throw new UnsupportedOperationException(
+                    "This method is not supported in target SDK version " + version + " and above");
+        }
+    }
+
+    // Checks whether the calling app can use the legacy routing API (startUsingNetworkFeature,
+    // stopUsingNetworkFeature, requestRouteToHost), and if not throw UnsupportedOperationException.
+    // TODO: convert the existing system users (Tethering, GnssLocationProvider) to the new APIs and
+    // remove these exemptions. Note that this check is not secure, and apps can still access these
+    // functions by accessing ConnectivityService directly. However, it should be clear that doing
+    // so is unsupported and may break in the future. http://b/22728205
+    private void checkLegacyRoutingApiAccess() {
+        unsupportedStartingFrom(VERSION_CODES.M);
+    }
+
+    /**
+     * Binds host resolutions performed by this process to {@code network}.
+     * {@link #bindProcessToNetwork} takes precedence over this setting.
+     *
+     * @param network The {@link Network} to bind host resolutions from the current process to, or
+     *                {@code null} to clear the current binding.
+     * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid.
+     * @hide
+     * @deprecated This is strictly for legacy usage to support {@link #startUsingNetworkFeature}.
+     */
+    @Deprecated
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static boolean setProcessDefaultNetworkForHostResolution(Network network) {
+        return NetworkUtils.bindProcessToNetworkForHostResolution(
+                (network == null) ? NETID_UNSET : network.getNetIdForResolv());
+    }
+
+    /**
+     * Device is not restricting metered network activity while application is running on
+     * background.
+     */
+    public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1;
+
+    /**
+     * Device is restricting metered network activity while application is running on background,
+     * but application is allowed to bypass it.
+     * <p>
+     * In this state, application should take action to mitigate metered network access.
+     * For example, a music streaming application should switch to a low-bandwidth bitrate.
+     */
+    public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2;
+
+    /**
+     * Device is restricting metered network activity while application is running on background.
+     * <p>
+     * In this state, application should not try to use the network while running on background,
+     * because it would be denied.
+     */
+    public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3;
+
+    /**
+     * A change in the background metered network activity restriction has occurred.
+     * <p>
+     * Applications should call {@link #getRestrictBackgroundStatus()} to check if the restriction
+     * applies to them.
+     * <p>
+     * This is only sent to registered receivers, not manifest receivers.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_RESTRICT_BACKGROUND_CHANGED =
+            "android.net.conn.RESTRICT_BACKGROUND_CHANGED";
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = false, value = {
+            RESTRICT_BACKGROUND_STATUS_DISABLED,
+            RESTRICT_BACKGROUND_STATUS_WHITELISTED,
+            RESTRICT_BACKGROUND_STATUS_ENABLED,
+    })
+    public @interface RestrictBackgroundStatus {
+    }
+
+    /**
+     * Determines if the calling application is subject to metered network restrictions while
+     * running on background.
+     *
+     * @return {@link #RESTRICT_BACKGROUND_STATUS_DISABLED},
+     * {@link #RESTRICT_BACKGROUND_STATUS_ENABLED},
+     * or {@link #RESTRICT_BACKGROUND_STATUS_WHITELISTED}
+     */
+    public @RestrictBackgroundStatus int getRestrictBackgroundStatus() {
+        try {
+            return mService.getRestrictBackgroundStatusByCaller();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * The network watchlist is a list of domains and IP addresses that are associated with
+     * potentially harmful apps. This method returns the SHA-256 of the watchlist config file
+     * currently used by the system for validation purposes.
+     *
+     * @return Hash of network watchlist config file. Null if config does not exist.
+     */
+    @Nullable
+    public byte[] getNetworkWatchlistConfigHash() {
+        try {
+            return mService.getNetworkWatchlistConfigHash();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Unable to get watchlist config hash");
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the {@code uid} of the owner of a network connection.
+     *
+     * @param protocol The protocol of the connection. Only {@code IPPROTO_TCP} and {@code
+     *     IPPROTO_UDP} currently supported.
+     * @param local The local {@link InetSocketAddress} of a connection.
+     * @param remote The remote {@link InetSocketAddress} of a connection.
+     * @return {@code uid} if the connection is found and the app has permission to observe it
+     *     (e.g., if it is associated with the calling VPN app's VpnService tunnel) or {@link
+     *     android.os.Process#INVALID_UID} if the connection is not found.
+     * @throws {@link SecurityException} if the caller is not the active VpnService for the current
+     *     user.
+     * @throws {@link IllegalArgumentException} if an unsupported protocol is requested.
+     */
+    public int getConnectionOwnerUid(
+            int protocol, @NonNull InetSocketAddress local, @NonNull InetSocketAddress remote) {
+        ConnectionInfo connectionInfo = new ConnectionInfo(protocol, local, remote);
+        try {
+            return mService.getConnectionOwnerUid(connectionInfo);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private void printStackTrace() {
+        if (DEBUG) {
+            final StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
+            final StringBuffer sb = new StringBuffer();
+            for (int i = 3; i < callStack.length; i++) {
+                final String stackTrace = callStack[i].toString();
+                if (stackTrace == null || stackTrace.contains("android.os")) {
+                    break;
+                }
+                sb.append(" [").append(stackTrace).append("]");
+            }
+            Log.d(TAG, "StackLog:" + sb.toString());
+        }
+    }
+
+    /** @hide */
+    public TestNetworkManager startOrGetTestNetworkManager() {
+        final IBinder tnBinder;
+        try {
+            tnBinder = mService.startOrGetTestNetworkService();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        return new TestNetworkManager(ITestNetworkManager.Stub.asInterface(tnBinder));
+    }
+
+    /** @hide */
+    public ConnectivityDiagnosticsManager createDiagnosticsManager() {
+        return new ConnectivityDiagnosticsManager(mContext, mService);
+    }
+
+    /**
+     * Simulates a Data Stall for the specified Network.
+     *
+     * <p>This method should only be used for tests.
+     *
+     * <p>The caller must be the owner of the specified Network. This simulates a data stall to
+     * have the system behave as if it had happened, but does not actually stall connectivity.
+     *
+     * @param detectionMethod The detection method used to identify the Data Stall.
+     *                        See ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_*.
+     * @param timestampMillis The timestamp at which the stall 'occurred', in milliseconds, as per
+     *                        SystemClock.elapsedRealtime.
+     * @param network The Network for which a Data Stall is being simluated.
+     * @param extras The PersistableBundle of extras included in the Data Stall notification.
+     * @throws SecurityException if the caller is not the owner of the given network.
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_TEST_NETWORKS,
+            android.Manifest.permission.NETWORK_STACK})
+    public void simulateDataStall(@DetectionMethod int detectionMethod, long timestampMillis,
+            @NonNull Network network, @NonNull PersistableBundle extras) {
+        try {
+            mService.simulateDataStall(detectionMethod, timestampMillis, network, extras);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    @NonNull
+    private final List<QosCallbackConnection> mQosCallbackConnections = new ArrayList<>();
+
+    /**
+     * Registers a {@link QosSocketInfo} with an associated {@link QosCallback}.  The callback will
+     * receive available QoS events related to the {@link Network} and local ip + port
+     * specified within socketInfo.
+     * <p/>
+     * The same {@link QosCallback} must be unregistered before being registered a second time,
+     * otherwise {@link QosCallbackRegistrationException} is thrown.
+     * <p/>
+     * This API does not, in itself, require any permission if called with a network that is not
+     * restricted. However, the underlying implementation currently only supports the IMS network,
+     * which is always restricted. That means non-preinstalled callers can't possibly find this API
+     * useful, because they'd never be called back on networks that they would have access to.
+     *
+     * @throws SecurityException if {@link QosSocketInfo#getNetwork()} is restricted and the app is
+     * missing CONNECTIVITY_USE_RESTRICTED_NETWORKS permission.
+     * @throws QosCallback.QosCallbackRegistrationException if qosCallback is already registered.
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     *
+     * Exceptions after the time of registration is passed through
+     * {@link QosCallback#onError(QosCallbackException)}.  see: {@link QosCallbackException}.
+     *
+     * @param socketInfo the socket information used to match QoS events
+     * @param executor The executor on which the callback will be invoked. The provided
+     *                 {@link Executor} must run callback sequentially, otherwise the order of
+     *                 callbacks cannot be guaranteed.onQosCallbackRegistered
+     * @param callback receives qos events that satisfy socketInfo
+     *
+     * @hide
+     */
+    @SystemApi
+    public void registerQosCallback(@NonNull final QosSocketInfo socketInfo,
+            @CallbackExecutor @NonNull final Executor executor,
+            @NonNull final QosCallback callback) {
+        Objects.requireNonNull(socketInfo, "socketInfo must be non-null");
+        Objects.requireNonNull(executor, "executor must be non-null");
+        Objects.requireNonNull(callback, "callback must be non-null");
+
+        try {
+            synchronized (mQosCallbackConnections) {
+                if (getQosCallbackConnection(callback) == null) {
+                    final QosCallbackConnection connection =
+                            new QosCallbackConnection(this, callback, executor);
+                    mQosCallbackConnections.add(connection);
+                    mService.registerQosSocketCallback(socketInfo, connection);
+                } else {
+                    Log.e(TAG, "registerQosCallback: Callback already registered");
+                    throw new QosCallbackRegistrationException();
+                }
+            }
+        } catch (final RemoteException e) {
+            Log.e(TAG, "registerQosCallback: Error while registering ", e);
+
+            // The same unregister method method is called for consistency even though nothing
+            // will be sent to the ConnectivityService since the callback was never successfully
+            // registered.
+            unregisterQosCallback(callback);
+            e.rethrowFromSystemServer();
+        } catch (final ServiceSpecificException e) {
+            Log.e(TAG, "registerQosCallback: Error while registering ", e);
+            unregisterQosCallback(callback);
+            throw convertServiceException(e);
+        }
+    }
+
+    /**
+     * Unregisters the given {@link QosCallback}.  The {@link QosCallback} will no longer receive
+     * events once unregistered and can be registered a second time.
+     * <p/>
+     * If the {@link QosCallback} does not have an active registration, it is a no-op.
+     *
+     * @param callback the callback being unregistered
+     *
+     * @hide
+     */
+    @SystemApi
+    public void unregisterQosCallback(@NonNull final QosCallback callback) {
+        Objects.requireNonNull(callback, "The callback must be non-null");
+        try {
+            synchronized (mQosCallbackConnections) {
+                final QosCallbackConnection connection = getQosCallbackConnection(callback);
+                if (connection != null) {
+                    connection.stopReceivingMessages();
+                    mService.unregisterQosCallback(connection);
+                    mQosCallbackConnections.remove(connection);
+                } else {
+                    Log.d(TAG, "unregisterQosCallback: Callback not registered");
+                }
+            }
+        } catch (final RemoteException e) {
+            Log.e(TAG, "unregisterQosCallback: Error while unregistering ", e);
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Gets the connection related to the callback.
+     *
+     * @param callback the callback to look up
+     * @return the related connection
+     */
+    @Nullable
+    private QosCallbackConnection getQosCallbackConnection(final QosCallback callback) {
+        for (final QosCallbackConnection connection : mQosCallbackConnections) {
+            // Checking by reference here is intentional
+            if (connection.getCallback() == callback) {
+                return connection;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Request a network to satisfy a set of {@link NetworkCapabilities}, but
+     * does not cause any networks to retain the NET_CAPABILITY_FOREGROUND capability. This can
+     * be used to request that the system provide a network without causing the network to be
+     * in the foreground.
+     *
+     * <p>This method will attempt to find the best network that matches the passed
+     * {@link NetworkRequest}, and to bring up one that does if none currently satisfies the
+     * criteria. The platform will evaluate which network is the best at its own discretion.
+     * Throughput, latency, cost per byte, policy, user preference and other considerations
+     * may be factored in the decision of what is considered the best network.
+     *
+     * <p>As long as this request is outstanding, the platform will try to maintain the best network
+     * matching this request, while always attempting to match the request to a better network if
+     * possible. If a better match is found, the platform will switch this request to the now-best
+     * network and inform the app of the newly best network by invoking
+     * {@link NetworkCallback#onAvailable(Network)} on the provided callback. Note that the platform
+     * will not try to maintain any other network than the best one currently matching the request:
+     * a network not matching any network request may be disconnected at any time.
+     *
+     * <p>For example, an application could use this method to obtain a connected cellular network
+     * even if the device currently has a data connection over Ethernet. This may cause the cellular
+     * radio to consume additional power. Or, an application could inform the system that it wants
+     * a network supporting sending MMSes and have the system let it know about the currently best
+     * MMS-supporting network through the provided {@link NetworkCallback}.
+     *
+     * <p>The status of the request can be followed by listening to the various callbacks described
+     * in {@link NetworkCallback}. The {@link Network} object passed to the callback methods can be
+     * used to direct traffic to the network (although accessing some networks may be subject to
+     * holding specific permissions). Callers will learn about the specific characteristics of the
+     * network through
+     * {@link NetworkCallback#onCapabilitiesChanged(Network, NetworkCapabilities)} and
+     * {@link NetworkCallback#onLinkPropertiesChanged(Network, LinkProperties)}. The methods of the
+     * provided {@link NetworkCallback} will only be invoked due to changes in the best network
+     * matching the request at any given time; therefore when a better network matching the request
+     * becomes available, the {@link NetworkCallback#onAvailable(Network)} method is called
+     * with the new network after which no further updates are given about the previously-best
+     * network, unless it becomes the best again at some later time. All callbacks are invoked
+     * in order on the same thread, which by default is a thread created by the framework running
+     * in the app.
+     *
+     * <p>This{@link NetworkRequest} will live until released via
+     * {@link #unregisterNetworkCallback(NetworkCallback)} or the calling application exits, at
+     * which point the system may let go of the network at any time.
+     *
+     * <p>It is presently unsupported to request a network with mutable
+     * {@link NetworkCapabilities} such as
+     * {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED} or
+     * {@link NetworkCapabilities#NET_CAPABILITY_CAPTIVE_PORTAL}
+     * as these {@code NetworkCapabilities} represent states that a particular
+     * network may never attain, and whether a network will attain these states
+     * is unknown prior to bringing up the network so the framework does not
+     * know how to go about satisfying a request with these capabilities.
+     *
+     * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+     * number of outstanding requests to 100 per app (identified by their UID), shared with
+     * all variants of this method, of {@link #registerNetworkCallback} as well as
+     * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+     * Requesting a network with this method will count toward this limit. If this limit is
+     * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+     * make sure to unregister the callbacks with
+     * {@link #unregisterNetworkCallback(NetworkCallback)}.
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+     *                        the callback must not be shared - it uniquely specifies this request.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     *                If null, the callback is invoked on the default internal Handler.
+     * @throws IllegalArgumentException if {@code request} contains invalid network capabilities.
+     * @throws SecurityException if missing the appropriate permissions.
+     * @throws RuntimeException if the app already has too many callbacks registered.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @SuppressLint("ExecutorRegistration")
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_STACK,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+    })
+    public void requestBackgroundNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback,
+            @SuppressLint("ListenerLast") @NonNull Handler handler) {
+        final NetworkCapabilities nc = request.networkCapabilities;
+        sendRequestForNetwork(nc, networkCallback, 0, BACKGROUND_REQUEST,
+                TYPE_NONE, new CallbackHandler(handler));
+    }
+
+    /**
+     * Used by automotive devices to set the network preferences used to direct traffic at an
+     * application level as per the given OemNetworkPreferences. An example use-case would be an
+     * automotive OEM wanting to provide connectivity for applications critical to the usage of a
+     * vehicle via a particular network.
+     *
+     * Calling this will overwrite the existing preference.
+     *
+     * @param preference {@link OemNetworkPreferences} The application network preference to be set.
+     * @param executor the executor on which listener will be invoked.
+     * @param listener {@link OnSetOemNetworkPreferenceListener} optional listener used to
+     *                  communicate completion of setOemNetworkPreference(). This will only be
+     *                  called once upon successful completion of setOemNetworkPreference().
+     * @throws IllegalArgumentException if {@code preference} contains invalid preference values.
+     * @throws SecurityException if missing the appropriate permissions.
+     * @throws UnsupportedOperationException if called on a non-automotive device.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE)
+    public void setOemNetworkPreference(@NonNull final OemNetworkPreferences preference,
+            @Nullable @CallbackExecutor final Executor executor,
+            @Nullable final Runnable listener) {
+        Objects.requireNonNull(preference, "OemNetworkPreferences must be non-null");
+        if (null != listener) {
+            Objects.requireNonNull(executor, "Executor must be non-null");
+        }
+        final IOnCompleteListener listenerInternal = listener == null ? null :
+                new IOnCompleteListener.Stub() {
+                    @Override
+                    public void onComplete() {
+                        executor.execute(listener::run);
+                    }
+        };
+
+        try {
+            mService.setOemNetworkPreference(preference, listenerInternal);
+        } catch (RemoteException e) {
+            Log.e(TAG, "setOemNetworkPreference() failed for preference: " + preference.toString());
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Request that a user profile is put by default on a network matching a given preference.
+     *
+     * See the documentation for the individual preferences for a description of the supported
+     * behaviors.
+     *
+     * @param profile the profile concerned.
+     * @param preference the preference for this profile.
+     * @param executor an executor to execute the listener on. Optional if listener is null.
+     * @param listener an optional listener to listen for completion of the operation.
+     * @throws IllegalArgumentException if {@code profile} is not a valid user profile.
+     * @throws SecurityException if missing the appropriate permissions.
+     * @hide
+     */
+    // This function is for establishing per-profile default networking and can only be called by
+    // the device policy manager, running as the system server. It would make no sense to call it
+    // on a context for a user because it does not establish a setting on behalf of a user, rather
+    // it establishes a setting for a user on behalf of the DPM.
+    @SuppressLint({"UserHandle"})
+    @SystemApi(client = MODULE_LIBRARIES)
+    @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+    public void setProfileNetworkPreference(@NonNull final UserHandle profile,
+            @ProfileNetworkPreference final int preference,
+            @Nullable @CallbackExecutor final Executor executor,
+            @Nullable final Runnable listener) {
+        if (null != listener) {
+            Objects.requireNonNull(executor, "Pass a non-null executor, or a null listener");
+        }
+        final IOnCompleteListener proxy;
+        if (null == listener) {
+            proxy = null;
+        } else {
+            proxy = new IOnCompleteListener.Stub() {
+                @Override
+                public void onComplete() {
+                    executor.execute(listener::run);
+                }
+            };
+        }
+        try {
+            mService.setProfileNetworkPreference(profile, preference, proxy);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    // The first network ID of IPSec tunnel interface.
+    private static final int TUN_INTF_NETID_START = 0xFC00; // 0xFC00 = 64512
+    // The network ID range of IPSec tunnel interface.
+    private static final int TUN_INTF_NETID_RANGE = 0x0400; // 0x0400 = 1024
+
+    /**
+     * Get the network ID range reserved for IPSec tunnel interfaces.
+     *
+     * @return A Range which indicates the network ID range of IPSec tunnel interface.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @NonNull
+    public static Range<Integer> getIpSecNetIdRange() {
+        return new Range(TUN_INTF_NETID_START, TUN_INTF_NETID_START + TUN_INTF_NETID_RANGE - 1);
+    }
+}
diff --git a/framework/src/android/net/ConnectivityResources.java b/framework/src/android/net/ConnectivityResources.java
new file mode 100644
index 0000000..18f0de0
--- /dev/null
+++ b/framework/src/android/net/ConnectivityResources.java
@@ -0,0 +1,108 @@
+/*
+ * 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.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.List;
+
+/**
+ * Utility to obtain the {@link com.android.server.ConnectivityService} {@link Resources}, in the
+ * ServiceConnectivityResources APK.
+ * @hide
+ */
+public class ConnectivityResources {
+    private static final String RESOURCES_APK_INTENT =
+            "com.android.server.connectivity.intent.action.SERVICE_CONNECTIVITY_RESOURCES_APK";
+    private static final String RES_PKG_DIR = "/apex/com.android.tethering/";
+
+    @NonNull
+    private final Context mContext;
+
+    @Nullable
+    private Context mResourcesContext = null;
+
+    @Nullable
+    private static Context sTestResourcesContext = null;
+
+    public ConnectivityResources(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Convenience method to mock all resources for the duration of a test.
+     *
+     * Call with a null context to reset after the test.
+     */
+    @VisibleForTesting
+    public static void setResourcesContextForTest(@Nullable Context testContext) {
+        sTestResourcesContext = testContext;
+    }
+
+    /**
+     * Get the {@link Context} of the resources package.
+     */
+    public synchronized Context getResourcesContext() {
+        if (sTestResourcesContext != null) {
+            return sTestResourcesContext;
+        }
+
+        if (mResourcesContext != null) {
+            return mResourcesContext;
+        }
+
+        final List<ResolveInfo> pkgs = mContext.getPackageManager()
+                .queryIntentActivities(new Intent(RESOURCES_APK_INTENT), MATCH_SYSTEM_ONLY);
+        pkgs.removeIf(pkg -> !pkg.activityInfo.applicationInfo.sourceDir.startsWith(RES_PKG_DIR));
+        if (pkgs.size() > 1) {
+            Log.wtf(ConnectivityResources.class.getSimpleName(),
+                    "More than one package found: " + pkgs);
+        }
+        if (pkgs.isEmpty()) {
+            throw new IllegalStateException("No connectivity resource package found");
+        }
+
+        final Context pkgContext;
+        try {
+            pkgContext = mContext.createPackageContext(
+                    pkgs.get(0).activityInfo.applicationInfo.packageName, 0 /* flags */);
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new IllegalStateException("Resolved package not found", e);
+        }
+
+        mResourcesContext = pkgContext;
+        return pkgContext;
+    }
+
+    /**
+     * Get the {@link Resources} of the ServiceConnectivityResources APK.
+     */
+    public Resources get() {
+        return getResourcesContext().getResources();
+    }
+}
diff --git a/framework/src/android/net/ConnectivitySettingsManager.java b/framework/src/android/net/ConnectivitySettingsManager.java
new file mode 100644
index 0000000..4644e4f
--- /dev/null
+++ b/framework/src/android/net/ConnectivitySettingsManager.java
@@ -0,0 +1,1051 @@
+/*
+ * 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.net.ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER;
+import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_PERFORMANCE;
+import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY;
+
+import static com.android.net.module.util.ConnectivitySettingsUtils.getPrivateDnsModeAsString;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.net.ConnectivityManager.MultipathPreference;
+import android.os.Process;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Range;
+
+import com.android.net.module.util.ConnectivitySettingsUtils;
+import com.android.net.module.util.ProxyUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
+import java.util.List;
+import java.util.Set;
+import java.util.StringJoiner;
+
+/**
+ * A manager class for connectivity module settings.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public class ConnectivitySettingsManager {
+
+    private ConnectivitySettingsManager() {}
+
+    /** Data activity timeout settings */
+
+    /**
+     * Inactivity timeout to track mobile data activity.
+     *
+     * If set to a positive integer, it indicates the inactivity timeout value in seconds to
+     * infer the data activity of mobile network. After a period of no activity on mobile
+     * networks with length specified by the timeout, an {@code ACTION_DATA_ACTIVITY_CHANGE}
+     * intent is fired to indicate a transition of network status from "active" to "idle". Any
+     * subsequent activity on mobile networks triggers the firing of {@code
+     * ACTION_DATA_ACTIVITY_CHANGE} intent indicating transition from "idle" to "active".
+     *
+     * Network activity refers to transmitting or receiving data on the network interfaces.
+     *
+     * Tracking is disabled if set to zero or negative value.
+     *
+     * @hide
+     */
+    public static final String DATA_ACTIVITY_TIMEOUT_MOBILE = "data_activity_timeout_mobile";
+
+    /**
+     * Timeout to tracking Wifi data activity. Same as {@code DATA_ACTIVITY_TIMEOUT_MOBILE}
+     * but for Wifi network.
+     *
+     * @hide
+     */
+    public static final String DATA_ACTIVITY_TIMEOUT_WIFI = "data_activity_timeout_wifi";
+
+    /** Dns resolver settings */
+
+    /**
+     * Sample validity in seconds to configure for the system DNS resolver.
+     *
+     * @hide
+     */
+    public static final String DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS =
+            "dns_resolver_sample_validity_seconds";
+
+    /**
+     * Success threshold in percent for use with the system DNS resolver.
+     *
+     * @hide
+     */
+    public static final String DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT =
+            "dns_resolver_success_threshold_percent";
+
+    /**
+     * Minimum number of samples needed for statistics to be considered meaningful in the
+     * system DNS resolver.
+     *
+     * @hide
+     */
+    public static final String DNS_RESOLVER_MIN_SAMPLES = "dns_resolver_min_samples";
+
+    /**
+     * Maximum number taken into account for statistics purposes in the system DNS resolver.
+     *
+     * @hide
+     */
+    public static final String DNS_RESOLVER_MAX_SAMPLES = "dns_resolver_max_samples";
+
+    private static final int DNS_RESOLVER_DEFAULT_MIN_SAMPLES = 8;
+    private static final int DNS_RESOLVER_DEFAULT_MAX_SAMPLES = 64;
+
+    /** Network switch notification settings */
+
+    /**
+     * The maximum number of notifications shown in 24 hours when switching networks.
+     *
+     * @hide
+     */
+    public static final String NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT =
+            "network_switch_notification_daily_limit";
+
+    /**
+     * The minimum time in milliseconds between notifications when switching networks.
+     *
+     * @hide
+     */
+    public static final String NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS =
+            "network_switch_notification_rate_limit_millis";
+
+    /** Captive portal settings */
+
+    /**
+     * The URL used for HTTP captive portal detection upon a new connection.
+     * A 204 response code from the server is used for validation.
+     *
+     * @hide
+     */
+    public static final String CAPTIVE_PORTAL_HTTP_URL = "captive_portal_http_url";
+
+    /**
+     * What to do when connecting a network that presents a captive portal.
+     * Must be one of the CAPTIVE_PORTAL_MODE_* constants below.
+     *
+     * The default for this setting is CAPTIVE_PORTAL_MODE_PROMPT.
+     *
+     * @hide
+     */
+    public static final String CAPTIVE_PORTAL_MODE = "captive_portal_mode";
+
+    /**
+     * Don't attempt to detect captive portals.
+     */
+    public static final int CAPTIVE_PORTAL_MODE_IGNORE = 0;
+
+    /**
+     * When detecting a captive portal, display a notification that
+     * prompts the user to sign in.
+     */
+    public static final int CAPTIVE_PORTAL_MODE_PROMPT = 1;
+
+    /**
+     * When detecting a captive portal, immediately disconnect from the
+     * network and do not reconnect to that network in the future.
+     */
+    public static final int CAPTIVE_PORTAL_MODE_AVOID = 2;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            CAPTIVE_PORTAL_MODE_IGNORE,
+            CAPTIVE_PORTAL_MODE_PROMPT,
+            CAPTIVE_PORTAL_MODE_AVOID,
+    })
+    public @interface CaptivePortalMode {}
+
+    /** Global http proxy settings */
+
+    /**
+     * Host name for global http proxy. Set via ConnectivityManager.
+     *
+     * @hide
+     */
+    public static final String GLOBAL_HTTP_PROXY_HOST = "global_http_proxy_host";
+
+    /**
+     * Integer host port for global http proxy. Set via ConnectivityManager.
+     *
+     * @hide
+     */
+    public static final String GLOBAL_HTTP_PROXY_PORT = "global_http_proxy_port";
+
+    /**
+     * Exclusion list for global proxy. This string contains a list of
+     * comma-separated domains where the global proxy does not apply.
+     * Domains should be listed in a comma- separated list. Example of
+     * acceptable formats: ".domain1.com,my.domain2.com" Use
+     * ConnectivityManager to set/get.
+     *
+     * @hide
+     */
+    public static final String GLOBAL_HTTP_PROXY_EXCLUSION_LIST =
+            "global_http_proxy_exclusion_list";
+
+    /**
+     * The location PAC File for the proxy.
+     *
+     * @hide
+     */
+    public static final String GLOBAL_HTTP_PROXY_PAC = "global_proxy_pac_url";
+
+    /** Private dns settings */
+
+    /**
+     * The requested Private DNS mode (string), and an accompanying specifier (string).
+     *
+     * Currently, the specifier holds the chosen provider name when the mode requests
+     * a specific provider. It may be used to store the provider name even when the
+     * mode changes so that temporarily disabling and re-enabling the specific
+     * provider mode does not necessitate retyping the provider hostname.
+     *
+     * @hide
+     */
+    public static final String PRIVATE_DNS_MODE = "private_dns_mode";
+
+    /**
+     * The specific Private DNS provider name.
+     *
+     * @hide
+     */
+    public static final String PRIVATE_DNS_SPECIFIER = "private_dns_specifier";
+
+    /**
+     * Forced override of the default mode (hardcoded as "automatic", nee "opportunistic").
+     * This allows changing the default mode without effectively disabling other modes,
+     * all of which require explicit user action to enable/configure. See also b/79719289.
+     *
+     * Value is a string, suitable for assignment to PRIVATE_DNS_MODE above.
+     *
+     * @hide
+     */
+    public static final String PRIVATE_DNS_DEFAULT_MODE = "private_dns_default_mode";
+
+    /** Other settings */
+
+    /**
+     * The number of milliseconds to hold on to a PendingIntent based request. This delay gives
+     * the receivers of the PendingIntent an opportunity to make a new network request before
+     * the Network satisfying the request is potentially removed.
+     *
+     * @hide
+     */
+    public static final String CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS =
+            "connectivity_release_pending_intent_delay_ms";
+
+    /**
+     * Whether the mobile data connection should remain active even when higher
+     * priority networks like WiFi are active, to help make network switching faster.
+     *
+     * See ConnectivityService for more info.
+     *
+     * (0 = disabled, 1 = enabled)
+     *
+     * @hide
+     */
+    public static final String MOBILE_DATA_ALWAYS_ON = "mobile_data_always_on";
+
+    /**
+     * Whether the wifi data connection should remain active even when higher
+     * priority networks like Ethernet are active, to keep both networks.
+     * In the case where higher priority networks are connected, wifi will be
+     * unused unless an application explicitly requests to use it.
+     *
+     * See ConnectivityService for more info.
+     *
+     * (0 = disabled, 1 = enabled)
+     *
+     * @hide
+     */
+    public static final String WIFI_ALWAYS_REQUESTED = "wifi_always_requested";
+
+    /**
+     * Whether to automatically switch away from wifi networks that lose Internet access.
+     * Only meaningful if config_networkAvoidBadWifi is set to 0, otherwise the system always
+     * avoids such networks. Valid values are:
+     *
+     * 0: Don't avoid bad wifi, don't prompt the user. Get stuck on bad wifi like it's 2013.
+     * null: Ask the user whether to switch away from bad wifi.
+     * 1: Avoid bad wifi.
+     *
+     * @hide
+     */
+    public static final String NETWORK_AVOID_BAD_WIFI = "network_avoid_bad_wifi";
+
+    /**
+     * Don't avoid bad wifi, don't prompt the user. Get stuck on bad wifi like it's 2013.
+     */
+    public static final int NETWORK_AVOID_BAD_WIFI_IGNORE = 0;
+
+    /**
+     * Ask the user whether to switch away from bad wifi.
+     */
+    public static final int NETWORK_AVOID_BAD_WIFI_PROMPT = 1;
+
+    /**
+     * Avoid bad wifi.
+     */
+    public static final int NETWORK_AVOID_BAD_WIFI_AVOID = 2;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            NETWORK_AVOID_BAD_WIFI_IGNORE,
+            NETWORK_AVOID_BAD_WIFI_PROMPT,
+            NETWORK_AVOID_BAD_WIFI_AVOID,
+    })
+    public @interface NetworkAvoidBadWifi {}
+
+    /**
+     * User setting for ConnectivityManager.getMeteredMultipathPreference(). This value may be
+     * overridden by the system based on device or application state. If null, the value
+     * specified by config_networkMeteredMultipathPreference is used.
+     *
+     * @hide
+     */
+    public static final String NETWORK_METERED_MULTIPATH_PREFERENCE =
+            "network_metered_multipath_preference";
+
+    /**
+     * A list of uids that should go on cellular networks in preference even when higher-priority
+     * networks are connected.
+     *
+     * @hide
+     */
+    public static final String MOBILE_DATA_PREFERRED_UIDS = "mobile_data_preferred_uids";
+
+    /**
+     * One of the private DNS modes that indicates the private DNS mode is off.
+     */
+    public static final int PRIVATE_DNS_MODE_OFF = ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OFF;
+
+    /**
+     * One of the private DNS modes that indicates the private DNS mode is automatic, which
+     * will try to use the current DNS as private DNS.
+     */
+    public static final int PRIVATE_DNS_MODE_OPPORTUNISTIC =
+            ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+
+    /**
+     * One of the private DNS modes that indicates the private DNS mode is strict and the
+     * {@link #PRIVATE_DNS_SPECIFIER} is required, which will try to use the value of
+     * {@link #PRIVATE_DNS_SPECIFIER} as private DNS.
+     */
+    public static final int PRIVATE_DNS_MODE_PROVIDER_HOSTNAME =
+            ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            PRIVATE_DNS_MODE_OFF,
+            PRIVATE_DNS_MODE_OPPORTUNISTIC,
+            PRIVATE_DNS_MODE_PROVIDER_HOSTNAME,
+    })
+    public @interface PrivateDnsMode {}
+
+    /**
+     * A list of uids that is allowed to use restricted networks.
+     *
+     * @hide
+     */
+    public static final String UIDS_ALLOWED_ON_RESTRICTED_NETWORKS =
+            "uids_allowed_on_restricted_networks";
+
+    /**
+     * Get mobile data activity timeout from {@link Settings}.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @param def The default timeout if no setting value.
+     * @return The {@link Duration} of timeout to track mobile data activity.
+     */
+    @NonNull
+    public static Duration getMobileDataActivityTimeout(@NonNull Context context,
+            @NonNull Duration def) {
+        final int timeout = Settings.Global.getInt(
+                context.getContentResolver(), DATA_ACTIVITY_TIMEOUT_MOBILE, (int) def.getSeconds());
+        return Duration.ofSeconds(timeout);
+    }
+
+    /**
+     * Set mobile data activity timeout to {@link Settings}.
+     * Tracking is disabled if set to zero or negative value.
+     *
+     * Note: Only use the number of seconds in this duration, lower second(nanoseconds) will be
+     * ignored.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param timeout The mobile data activity timeout.
+     */
+    public static void setMobileDataActivityTimeout(@NonNull Context context,
+            @NonNull Duration timeout) {
+        Settings.Global.putInt(context.getContentResolver(), DATA_ACTIVITY_TIMEOUT_MOBILE,
+                (int) timeout.getSeconds());
+    }
+
+    /**
+     * Get wifi data activity timeout from {@link Settings}.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @param def The default timeout if no setting value.
+     * @return The {@link Duration} of timeout to track wifi data activity.
+     */
+    @NonNull
+    public static Duration getWifiDataActivityTimeout(@NonNull Context context,
+            @NonNull Duration def) {
+        final int timeout = Settings.Global.getInt(
+                context.getContentResolver(), DATA_ACTIVITY_TIMEOUT_WIFI, (int) def.getSeconds());
+        return Duration.ofSeconds(timeout);
+    }
+
+    /**
+     * Set wifi data activity timeout to {@link Settings}.
+     * Tracking is disabled if set to zero or negative value.
+     *
+     * Note: Only use the number of seconds in this duration, lower second(nanoseconds) will be
+     * ignored.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param timeout The wifi data activity timeout.
+     */
+    public static void setWifiDataActivityTimeout(@NonNull Context context,
+            @NonNull Duration timeout) {
+        Settings.Global.putInt(context.getContentResolver(), DATA_ACTIVITY_TIMEOUT_WIFI,
+                (int) timeout.getSeconds());
+    }
+
+    /**
+     * Get dns resolver sample validity duration from {@link Settings}.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @param def The default duration if no setting value.
+     * @return The {@link Duration} of sample validity duration to configure for the system DNS
+     *         resolver.
+     */
+    @NonNull
+    public static Duration getDnsResolverSampleValidityDuration(@NonNull Context context,
+            @NonNull Duration def) {
+        final int duration = Settings.Global.getInt(context.getContentResolver(),
+                DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS, (int) def.getSeconds());
+        return Duration.ofSeconds(duration);
+    }
+
+    /**
+     * Set dns resolver sample validity duration to {@link Settings}. The duration must be a
+     * positive number of seconds.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param duration The sample validity duration.
+     */
+    public static void setDnsResolverSampleValidityDuration(@NonNull Context context,
+            @NonNull Duration duration) {
+        final int time = (int) duration.getSeconds();
+        if (time <= 0) {
+            throw new IllegalArgumentException("Invalid duration");
+        }
+        Settings.Global.putInt(
+                context.getContentResolver(), DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS, time);
+    }
+
+    /**
+     * Get dns resolver success threshold percent from {@link Settings}.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @param def The default value if no setting value.
+     * @return The success threshold in percent for use with the system DNS resolver.
+     */
+    public static int getDnsResolverSuccessThresholdPercent(@NonNull Context context, int def) {
+        return Settings.Global.getInt(
+                context.getContentResolver(), DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT, def);
+    }
+
+    /**
+     * Set dns resolver success threshold percent to {@link Settings}. The threshold percent must
+     * be 0~100.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param percent The success threshold percent.
+     */
+    public static void setDnsResolverSuccessThresholdPercent(@NonNull Context context,
+            @IntRange(from = 0, to = 100) int percent) {
+        if (percent < 0 || percent > 100) {
+            throw new IllegalArgumentException("Percent must be 0~100");
+        }
+        Settings.Global.putInt(
+                context.getContentResolver(), DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT, percent);
+    }
+
+    /**
+     * Get dns resolver samples range from {@link Settings}.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @return The {@link Range<Integer>} of samples needed for statistics to be considered
+     *         meaningful in the system DNS resolver.
+     */
+    @NonNull
+    public static Range<Integer> getDnsResolverSampleRanges(@NonNull Context context) {
+        final int minSamples = Settings.Global.getInt(context.getContentResolver(),
+                DNS_RESOLVER_MIN_SAMPLES, DNS_RESOLVER_DEFAULT_MIN_SAMPLES);
+        final int maxSamples = Settings.Global.getInt(context.getContentResolver(),
+                DNS_RESOLVER_MAX_SAMPLES, DNS_RESOLVER_DEFAULT_MAX_SAMPLES);
+        return new Range<>(minSamples, maxSamples);
+    }
+
+    /**
+     * Set dns resolver samples range to {@link Settings}.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param range The samples range. The minimum number should be more than 0 and the maximum
+     *              number should be less that 64.
+     */
+    public static void setDnsResolverSampleRanges(@NonNull Context context,
+            @NonNull Range<Integer> range) {
+        if (range.getLower() < 0 || range.getUpper() > 64) {
+            throw new IllegalArgumentException("Argument must be 0~64");
+        }
+        Settings.Global.putInt(
+                context.getContentResolver(), DNS_RESOLVER_MIN_SAMPLES, range.getLower());
+        Settings.Global.putInt(
+                context.getContentResolver(), DNS_RESOLVER_MAX_SAMPLES, range.getUpper());
+    }
+
+    /**
+     * Get maximum count (from {@link Settings}) of switching network notifications shown in 24
+     * hours.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @param def The default value if no setting value.
+     * @return The maximum count of notifications shown in 24 hours when switching networks.
+     */
+    public static int getNetworkSwitchNotificationMaximumDailyCount(@NonNull Context context,
+            int def) {
+        return Settings.Global.getInt(
+                context.getContentResolver(), NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT, def);
+    }
+
+    /**
+     * Set maximum count (to {@link Settings}) of switching network notifications shown in 24 hours.
+     * The count must be at least 0.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param count The maximum count of switching network notifications shown in 24 hours.
+     */
+    public static void setNetworkSwitchNotificationMaximumDailyCount(@NonNull Context context,
+            @IntRange(from = 0) int count) {
+        if (count < 0) {
+            throw new IllegalArgumentException("Count must be 0~10.");
+        }
+        Settings.Global.putInt(
+                context.getContentResolver(), NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT, count);
+    }
+
+    /**
+     * Get minimum duration (from {@link Settings}) between each switching network notifications.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @param def The default time if no setting value.
+     * @return The minimum duration between notifications when switching networks.
+     */
+    @NonNull
+    public static Duration getNetworkSwitchNotificationRateDuration(@NonNull Context context,
+            @NonNull Duration def) {
+        final int duration = Settings.Global.getInt(context.getContentResolver(),
+                NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS, (int) def.toMillis());
+        return Duration.ofMillis(duration);
+    }
+
+    /**
+     * Set minimum duration (to {@link Settings}) between each switching network notifications.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param duration The minimum duration between notifications when switching networks.
+     */
+    public static void setNetworkSwitchNotificationRateDuration(@NonNull Context context,
+            @NonNull Duration duration) {
+        final int time = (int) duration.toMillis();
+        if (time < 0) {
+            throw new IllegalArgumentException("Invalid duration.");
+        }
+        Settings.Global.putInt(context.getContentResolver(),
+                NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS, time);
+    }
+
+    /**
+     * Get URL (from {@link Settings}) used for HTTP captive portal detection upon a new connection.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @return The URL used for HTTP captive portal detection upon a new connection.
+     */
+    @Nullable
+    public static String getCaptivePortalHttpUrl(@NonNull Context context) {
+        return Settings.Global.getString(context.getContentResolver(), CAPTIVE_PORTAL_HTTP_URL);
+    }
+
+    /**
+     * Set URL (to {@link Settings}) used for HTTP captive portal detection upon a new connection.
+     * This URL should respond with a 204 response to a GET request to indicate no captive portal is
+     * present. And this URL must be HTTP as redirect responses are used to find captive portal
+     * sign-in pages. If the URL set to null or be incorrect, it will result in captive portal
+     * detection failed and lost the connection.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param url The URL used for HTTP captive portal detection upon a new connection.
+     */
+    public static void setCaptivePortalHttpUrl(@NonNull Context context, @Nullable String url) {
+        Settings.Global.putString(context.getContentResolver(), CAPTIVE_PORTAL_HTTP_URL, url);
+    }
+
+    /**
+     * Get mode (from {@link Settings}) when connecting a network that presents a captive portal.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @param def The default mode if no setting value.
+     * @return The mode when connecting a network that presents a captive portal.
+     */
+    @CaptivePortalMode
+    public static int getCaptivePortalMode(@NonNull Context context,
+            @CaptivePortalMode int def) {
+        return Settings.Global.getInt(context.getContentResolver(), CAPTIVE_PORTAL_MODE, def);
+    }
+
+    /**
+     * Set mode (to {@link Settings}) when connecting a network that presents a captive portal.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param mode The mode when connecting a network that presents a captive portal.
+     */
+    public static void setCaptivePortalMode(@NonNull Context context, @CaptivePortalMode int mode) {
+        if (!(mode == CAPTIVE_PORTAL_MODE_IGNORE
+                || mode == CAPTIVE_PORTAL_MODE_PROMPT
+                || mode == CAPTIVE_PORTAL_MODE_AVOID)) {
+            throw new IllegalArgumentException("Invalid captive portal mode");
+        }
+        Settings.Global.putInt(context.getContentResolver(), CAPTIVE_PORTAL_MODE, mode);
+    }
+
+    /**
+     * Get the global HTTP proxy applied to the device, or null if none.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @return The {@link ProxyInfo} which build from global http proxy settings.
+     */
+    @Nullable
+    public static ProxyInfo getGlobalProxy(@NonNull Context context) {
+        final String host = Settings.Global.getString(
+                context.getContentResolver(), GLOBAL_HTTP_PROXY_HOST);
+        final int port = Settings.Global.getInt(
+                context.getContentResolver(), GLOBAL_HTTP_PROXY_PORT, 0 /* def */);
+        final String exclusionList = Settings.Global.getString(
+                context.getContentResolver(), GLOBAL_HTTP_PROXY_EXCLUSION_LIST);
+        final String pacFileUrl = Settings.Global.getString(
+                context.getContentResolver(), GLOBAL_HTTP_PROXY_PAC);
+
+        if (TextUtils.isEmpty(host) && TextUtils.isEmpty(pacFileUrl)) {
+            return null; // No global proxy.
+        }
+
+        if (TextUtils.isEmpty(pacFileUrl)) {
+            return ProxyInfo.buildDirectProxy(
+                    host, port, ProxyUtils.exclusionStringAsList(exclusionList));
+        } else {
+            return ProxyInfo.buildPacProxy(Uri.parse(pacFileUrl));
+        }
+    }
+
+    /**
+     * Set global http proxy settings from given {@link ProxyInfo}.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param proxyInfo The {@link ProxyInfo} for global http proxy settings which build from
+     *                    {@link ProxyInfo#buildPacProxy(Uri)} or
+     *                    {@link ProxyInfo#buildDirectProxy(String, int, List)}
+     */
+    public static void setGlobalProxy(@NonNull Context context, @NonNull ProxyInfo proxyInfo) {
+        final String host = proxyInfo.getHost();
+        final int port = proxyInfo.getPort();
+        final String exclusionList = proxyInfo.getExclusionListAsString();
+        final String pacFileUrl = proxyInfo.getPacFileUrl().toString();
+
+        if (TextUtils.isEmpty(pacFileUrl)) {
+            Settings.Global.putString(context.getContentResolver(), GLOBAL_HTTP_PROXY_HOST, host);
+            Settings.Global.putInt(context.getContentResolver(), GLOBAL_HTTP_PROXY_PORT, port);
+            Settings.Global.putString(
+                    context.getContentResolver(), GLOBAL_HTTP_PROXY_EXCLUSION_LIST, exclusionList);
+            Settings.Global.putString(
+                    context.getContentResolver(), GLOBAL_HTTP_PROXY_PAC, "" /* value */);
+        } else {
+            Settings.Global.putString(
+                    context.getContentResolver(), GLOBAL_HTTP_PROXY_PAC, pacFileUrl);
+            Settings.Global.putString(
+                    context.getContentResolver(), GLOBAL_HTTP_PROXY_HOST, "" /* value */);
+            Settings.Global.putInt(
+                    context.getContentResolver(), GLOBAL_HTTP_PROXY_PORT, 0 /* value */);
+            Settings.Global.putString(
+                    context.getContentResolver(), GLOBAL_HTTP_PROXY_EXCLUSION_LIST, "" /* value */);
+        }
+    }
+
+    /**
+     * Clear all global http proxy settings.
+     *
+     * @param context The {@link Context} to set the setting.
+     */
+    public static void clearGlobalProxy(@NonNull Context context) {
+        Settings.Global.putString(
+                context.getContentResolver(), GLOBAL_HTTP_PROXY_HOST, "" /* value */);
+        Settings.Global.putInt(
+                context.getContentResolver(), GLOBAL_HTTP_PROXY_PORT, 0 /* value */);
+        Settings.Global.putString(
+                context.getContentResolver(), GLOBAL_HTTP_PROXY_EXCLUSION_LIST, "" /* value */);
+        Settings.Global.putString(
+                context.getContentResolver(), GLOBAL_HTTP_PROXY_PAC, "" /* value */);
+    }
+
+    /**
+     * Get private DNS mode from settings.
+     *
+     * @param context The Context to query the private DNS mode from settings.
+     * @return A string of private DNS mode.
+     */
+    @PrivateDnsMode
+    public static int getPrivateDnsMode(@NonNull Context context) {
+        return ConnectivitySettingsUtils.getPrivateDnsMode(context);
+    }
+
+    /**
+     * Set private DNS mode to settings.
+     *
+     * @param context The {@link Context} to set the private DNS mode.
+     * @param mode The private dns mode. This should be one of the PRIVATE_DNS_MODE_* constants.
+     */
+    public static void setPrivateDnsMode(@NonNull Context context, @PrivateDnsMode int mode) {
+        ConnectivitySettingsUtils.setPrivateDnsMode(context, mode);
+    }
+
+    /**
+     * Get specific private dns provider name from {@link Settings}.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @return The specific private dns provider name, or null if no setting value.
+     */
+    @Nullable
+    public static String getPrivateDnsHostname(@NonNull Context context) {
+        return ConnectivitySettingsUtils.getPrivateDnsHostname(context);
+    }
+
+    /**
+     * Set specific private dns provider name to {@link Settings}.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param specifier The specific private dns provider name.
+     */
+    public static void setPrivateDnsHostname(@NonNull Context context, @Nullable String specifier) {
+        ConnectivitySettingsUtils.setPrivateDnsHostname(context, specifier);
+    }
+
+    /**
+     * Get default private dns mode from {@link Settings}.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @return The default private dns mode.
+     */
+    @PrivateDnsMode
+    @NonNull
+    public static String getPrivateDnsDefaultMode(@NonNull Context context) {
+        return Settings.Global.getString(context.getContentResolver(), PRIVATE_DNS_DEFAULT_MODE);
+    }
+
+    /**
+     * Set default private dns mode to {@link Settings}.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param mode The default private dns mode. This should be one of the PRIVATE_DNS_MODE_*
+     *             constants.
+     */
+    public static void setPrivateDnsDefaultMode(@NonNull Context context,
+            @NonNull @PrivateDnsMode int mode) {
+        if (!(mode == PRIVATE_DNS_MODE_OFF
+                || mode == PRIVATE_DNS_MODE_OPPORTUNISTIC
+                || mode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME)) {
+            throw new IllegalArgumentException("Invalid private dns mode");
+        }
+        Settings.Global.putString(context.getContentResolver(), PRIVATE_DNS_DEFAULT_MODE,
+                getPrivateDnsModeAsString(mode));
+    }
+
+    /**
+     * Get duration (from {@link Settings}) to keep a PendingIntent-based request.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @param def The default duration if no setting value.
+     * @return The duration to keep a PendingIntent-based request.
+     */
+    @NonNull
+    public static Duration getConnectivityKeepPendingIntentDuration(@NonNull Context context,
+            @NonNull Duration def) {
+        final int duration = Settings.Secure.getInt(context.getContentResolver(),
+                CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, (int) def.toMillis());
+        return Duration.ofMillis(duration);
+    }
+
+    /**
+     * Set duration (to {@link Settings}) to keep a PendingIntent-based request.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param duration The duration to keep a PendingIntent-based request.
+     */
+    public static void setConnectivityKeepPendingIntentDuration(@NonNull Context context,
+            @NonNull Duration duration) {
+        final int time = (int) duration.toMillis();
+        if (time < 0) {
+            throw new IllegalArgumentException("Invalid duration.");
+        }
+        Settings.Secure.putInt(
+                context.getContentResolver(), CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, time);
+    }
+
+    /**
+     * Read from {@link Settings} whether the mobile data connection should remain active
+     * even when higher priority networks are active.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @param def The default value if no setting value.
+     * @return Whether the mobile data connection should remain active even when higher
+     *         priority networks are active.
+     */
+    public static boolean getMobileDataAlwaysOn(@NonNull Context context, boolean def) {
+        final int enable = Settings.Global.getInt(
+                context.getContentResolver(), MOBILE_DATA_ALWAYS_ON, (def ? 1 : 0));
+        return (enable != 0) ? true : false;
+    }
+
+    /**
+     * Write into {@link Settings} whether the mobile data connection should remain active
+     * even when higher priority networks are active.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param enable Whether the mobile data connection should remain active even when higher
+     *               priority networks are active.
+     */
+    public static void setMobileDataAlwaysOn(@NonNull Context context, boolean enable) {
+        Settings.Global.putInt(
+                context.getContentResolver(), MOBILE_DATA_ALWAYS_ON, (enable ? 1 : 0));
+    }
+
+    /**
+     * Read from {@link Settings} whether the wifi data connection should remain active
+     * even when higher priority networks are active.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @param def The default value if no setting value.
+     * @return Whether the wifi data connection should remain active even when higher
+     *         priority networks are active.
+     */
+    public static boolean getWifiAlwaysRequested(@NonNull Context context, boolean def) {
+        final int enable = Settings.Global.getInt(
+                context.getContentResolver(), WIFI_ALWAYS_REQUESTED, (def ? 1 : 0));
+        return (enable != 0) ? true : false;
+    }
+
+    /**
+     * Write into {@link Settings} whether the wifi data connection should remain active
+     * even when higher priority networks are active.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param enable Whether the wifi data connection should remain active even when higher
+     *               priority networks are active
+     */
+    public static void setWifiAlwaysRequested(@NonNull Context context, boolean enable) {
+        Settings.Global.putInt(
+                context.getContentResolver(), WIFI_ALWAYS_REQUESTED, (enable ? 1 : 0));
+    }
+
+    /**
+     * Get avoid bad wifi setting from {@link Settings}.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @return The setting whether to automatically switch away from wifi networks that lose
+     *         internet access.
+     */
+    @NetworkAvoidBadWifi
+    public static int getNetworkAvoidBadWifi(@NonNull Context context) {
+        final String setting =
+                Settings.Global.getString(context.getContentResolver(), NETWORK_AVOID_BAD_WIFI);
+        if ("0".equals(setting)) {
+            return NETWORK_AVOID_BAD_WIFI_IGNORE;
+        } else if ("1".equals(setting)) {
+            return NETWORK_AVOID_BAD_WIFI_AVOID;
+        } else {
+            return NETWORK_AVOID_BAD_WIFI_PROMPT;
+        }
+    }
+
+    /**
+     * Set avoid bad wifi setting to {@link Settings}.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param value Whether to automatically switch away from wifi networks that lose internet
+     *              access.
+     */
+    public static void setNetworkAvoidBadWifi(@NonNull Context context,
+            @NetworkAvoidBadWifi int value) {
+        final String setting;
+        if (value == NETWORK_AVOID_BAD_WIFI_IGNORE) {
+            setting = "0";
+        } else if (value == NETWORK_AVOID_BAD_WIFI_AVOID) {
+            setting = "1";
+        } else if (value == NETWORK_AVOID_BAD_WIFI_PROMPT) {
+            setting = null;
+        } else {
+            throw new IllegalArgumentException("Invalid avoid bad wifi setting");
+        }
+        Settings.Global.putString(context.getContentResolver(), NETWORK_AVOID_BAD_WIFI, setting);
+    }
+
+    /**
+     * Get network metered multipath preference from {@link Settings}.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @return The network metered multipath preference which should be one of
+     *         ConnectivityManager#MULTIPATH_PREFERENCE_* value or null if the value specified
+     *         by config_networkMeteredMultipathPreference is used.
+     */
+    @Nullable
+    public static String getNetworkMeteredMultipathPreference(@NonNull Context context) {
+        return Settings.Global.getString(
+                context.getContentResolver(), NETWORK_METERED_MULTIPATH_PREFERENCE);
+    }
+
+    /**
+     * Set network metered multipath preference to {@link Settings}.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param preference The network metered multipath preference which should be one of
+     *                   ConnectivityManager#MULTIPATH_PREFERENCE_* value or null if the value
+     *                   specified by config_networkMeteredMultipathPreference is used.
+     */
+    public static void setNetworkMeteredMultipathPreference(@NonNull Context context,
+            @NonNull @MultipathPreference String preference) {
+        if (!(Integer.valueOf(preference) == MULTIPATH_PREFERENCE_HANDOVER
+                || Integer.valueOf(preference) == MULTIPATH_PREFERENCE_RELIABILITY
+                || Integer.valueOf(preference) == MULTIPATH_PREFERENCE_PERFORMANCE)) {
+            throw new IllegalArgumentException("Invalid private dns mode");
+        }
+        Settings.Global.putString(
+                context.getContentResolver(), NETWORK_METERED_MULTIPATH_PREFERENCE, preference);
+    }
+
+    private static Set<Integer> getUidSetFromString(@Nullable String uidList) {
+        final Set<Integer> uids = new ArraySet<>();
+        if (TextUtils.isEmpty(uidList)) {
+            return uids;
+        }
+        for (String uid : uidList.split(";")) {
+            uids.add(Integer.valueOf(uid));
+        }
+        return uids;
+    }
+
+    private static String getUidStringFromSet(@NonNull Set<Integer> uidList) {
+        final StringJoiner joiner = new StringJoiner(";");
+        for (Integer uid : uidList) {
+            if (uid < 0 || UserHandle.getAppId(uid) > Process.LAST_APPLICATION_UID) {
+                throw new IllegalArgumentException("Invalid uid");
+            }
+            joiner.add(uid.toString());
+        }
+        return joiner.toString();
+    }
+
+    /**
+     * Get the list of uids(from {@link Settings}) that should go on cellular networks in preference
+     * even when higher-priority networks are connected.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @return A list of uids that should go on cellular networks in preference even when
+     *         higher-priority networks are connected or null if no setting value.
+     */
+    @NonNull
+    public static Set<Integer> getMobileDataPreferredUids(@NonNull Context context) {
+        final String uidList = Settings.Secure.getString(
+                context.getContentResolver(), MOBILE_DATA_PREFERRED_UIDS);
+        return getUidSetFromString(uidList);
+    }
+
+    /**
+     * Set the list of uids(to {@link Settings}) that should go on cellular networks in preference
+     * even when higher-priority networks are connected.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param uidList A list of uids that should go on cellular networks in preference even when
+     *             higher-priority networks are connected.
+     */
+    public static void setMobileDataPreferredUids(@NonNull Context context,
+            @NonNull Set<Integer> uidList) {
+        final String uids = getUidStringFromSet(uidList);
+        Settings.Secure.putString(context.getContentResolver(), MOBILE_DATA_PREFERRED_UIDS, uids);
+    }
+
+    /**
+     * Get the list of uids (from {@link Settings}) allowed to use restricted networks.
+     *
+     * Access to restricted networks is controlled by the (preinstalled-only)
+     * CONNECTIVITY_USE_RESTRICTED_NETWORKS permission, but highly privileged
+     * callers can also set a list of uids that can access restricted networks.
+     *
+     * This is useful for example in some jurisdictions where government apps,
+     * that can't be preinstalled, must still have access to emergency services.
+     *
+     * @param context The {@link Context} to query the setting.
+     * @return A list of uids that is allowed to use restricted networks or null if no setting
+     *         value.
+     */
+    @NonNull
+    public static Set<Integer> getUidsAllowedOnRestrictedNetworks(@NonNull Context context) {
+        final String uidList = Settings.Global.getString(
+                context.getContentResolver(), UIDS_ALLOWED_ON_RESTRICTED_NETWORKS);
+        return getUidSetFromString(uidList);
+    }
+
+    /**
+     * Set the list of uids(from {@link Settings}) that is allowed to use restricted networks.
+     *
+     * @param context The {@link Context} to set the setting.
+     * @param uidList A list of uids that is allowed to use restricted networks.
+     */
+    public static void setUidsAllowedOnRestrictedNetworks(@NonNull Context context,
+            @NonNull Set<Integer> uidList) {
+        final String uids = getUidStringFromSet(uidList);
+        Settings.Global.putString(context.getContentResolver(), UIDS_ALLOWED_ON_RESTRICTED_NETWORKS,
+                uids);
+    }
+}
diff --git a/framework/src/android/net/ConnectivityThread.java b/framework/src/android/net/ConnectivityThread.java
new file mode 100644
index 0000000..0b218e7
--- /dev/null
+++ b/framework/src/android/net/ConnectivityThread.java
@@ -0,0 +1,56 @@
+/*
+ * 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.os.HandlerThread;
+import android.os.Looper;
+
+/**
+ * Shared singleton connectivity thread for the system.  This is a thread for
+ * connectivity operations such as AsyncChannel connections to system services.
+ * Various connectivity manager objects can use this singleton as a common
+ * resource for their handlers instead of creating separate threads of their own.
+ * @hide
+ */
+public final class ConnectivityThread extends HandlerThread {
+
+    // A class implementing the lazy holder idiom: the unique static instance
+    // of ConnectivityThread is instantiated in a thread-safe way (guaranteed by
+    // the language specs) the first time that Singleton is referenced in get()
+    // or getInstanceLooper().
+    private static class Singleton {
+        private static final ConnectivityThread INSTANCE = createInstance();
+    }
+
+    private ConnectivityThread() {
+        super("ConnectivityThread");
+    }
+
+    private static ConnectivityThread createInstance() {
+        ConnectivityThread t = new ConnectivityThread();
+        t.start();
+        return t;
+    }
+
+    public static ConnectivityThread get() {
+        return Singleton.INSTANCE;
+    }
+
+    public static Looper getInstanceLooper() {
+        return Singleton.INSTANCE.getLooper();
+    }
+}
diff --git a/framework/src/android/net/DhcpInfo.java b/framework/src/android/net/DhcpInfo.java
new file mode 100644
index 0000000..912df67
--- /dev/null
+++ b/framework/src/android/net/DhcpInfo.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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;
+
+/**
+ * A simple object for retrieving the results of a DHCP request.
+ */
+public class DhcpInfo implements Parcelable {
+    public int ipAddress;
+    public int gateway;
+    public int netmask;
+    public int dns1;
+    public int dns2;
+    public int serverAddress;
+
+    public int leaseDuration;
+
+    public DhcpInfo() {
+        super();
+    }
+
+    /** copy constructor {@hide} */
+    public DhcpInfo(DhcpInfo source) {
+        if (source != null) {
+            ipAddress = source.ipAddress;
+            gateway = source.gateway;
+            netmask = source.netmask;
+            dns1 = source.dns1;
+            dns2 = source.dns2;
+            serverAddress = source.serverAddress;
+            leaseDuration = source.leaseDuration;
+        }
+    }
+
+    public String toString() {
+        StringBuffer str = new StringBuffer();
+
+        str.append("ipaddr "); putAddress(str, ipAddress);
+        str.append(" gateway "); putAddress(str, gateway);
+        str.append(" netmask "); putAddress(str, netmask);
+        str.append(" dns1 "); putAddress(str, dns1);
+        str.append(" dns2 "); putAddress(str, dns2);
+        str.append(" DHCP server "); putAddress(str, serverAddress);
+        str.append(" lease ").append(leaseDuration).append(" seconds");
+
+        return str.toString();
+    }
+
+    private static void putAddress(StringBuffer buf, int addr) {
+        buf.append(NetworkUtils.intToInetAddress(addr).getHostAddress());
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(ipAddress);
+        dest.writeInt(gateway);
+        dest.writeInt(netmask);
+        dest.writeInt(dns1);
+        dest.writeInt(dns2);
+        dest.writeInt(serverAddress);
+        dest.writeInt(leaseDuration);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<DhcpInfo> CREATOR =
+        new Creator<DhcpInfo>() {
+            public DhcpInfo createFromParcel(Parcel in) {
+                DhcpInfo info = new DhcpInfo();
+                info.ipAddress = in.readInt();
+                info.gateway = in.readInt();
+                info.netmask = in.readInt();
+                info.dns1 = in.readInt();
+                info.dns2 = in.readInt();
+                info.serverAddress = in.readInt();
+                info.leaseDuration = in.readInt();
+                return info;
+            }
+
+            public DhcpInfo[] newArray(int size) {
+                return new DhcpInfo[size];
+            }
+        };
+}
diff --git a/framework/src/android/net/DnsResolver.java b/framework/src/android/net/DnsResolver.java
new file mode 100644
index 0000000..dac88ad
--- /dev/null
+++ b/framework/src/android/net/DnsResolver.java
@@ -0,0 +1,577 @@
+/*
+ * 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;
+
+import static android.net.NetworkUtils.getDnsNetwork;
+import static android.net.NetworkUtils.resNetworkCancel;
+import static android.net.NetworkUtils.resNetworkQuery;
+import static android.net.NetworkUtils.resNetworkResult;
+import static android.net.NetworkUtils.resNetworkSend;
+import static android.net.util.DnsUtils.haveIpv4;
+import static android.net.util.DnsUtils.haveIpv6;
+import static android.net.util.DnsUtils.rfc6724Sort;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+import static android.system.OsConstants.ENONET;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.CancellationSignal;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.system.ErrnoException;
+import android.util.Log;
+
+import com.android.net.module.util.DnsPacket;
+
+import java.io.FileDescriptor;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Dns resolver class for asynchronous dns querying
+ *
+ * Note that if a client sends a query with more than 1 record in the question section but
+ * the remote dns server does not support this, it may not respond at all, leading to a timeout.
+ *
+ */
+public final class DnsResolver {
+    private static final String TAG = "DnsResolver";
+    private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+    private static final int MAXPACKET = 8 * 1024;
+    private static final int SLEEP_TIME_MS = 2;
+
+    @IntDef(prefix = { "CLASS_" }, value = {
+            CLASS_IN
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface QueryClass {}
+    public static final int CLASS_IN = 1;
+
+    @IntDef(prefix = { "TYPE_" },  value = {
+            TYPE_A,
+            TYPE_AAAA
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface QueryType {}
+    public static final int TYPE_A = 1;
+    public static final int TYPE_AAAA = 28;
+
+    @IntDef(prefix = { "FLAG_" }, value = {
+            FLAG_EMPTY,
+            FLAG_NO_RETRY,
+            FLAG_NO_CACHE_STORE,
+            FLAG_NO_CACHE_LOOKUP
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface QueryFlag {}
+    public static final int FLAG_EMPTY = 0;
+    public static final int FLAG_NO_RETRY = 1 << 0;
+    public static final int FLAG_NO_CACHE_STORE = 1 << 1;
+    public static final int FLAG_NO_CACHE_LOOKUP = 1 << 2;
+
+    @IntDef(prefix = { "ERROR_" }, value = {
+            ERROR_PARSE,
+            ERROR_SYSTEM
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface DnsError {}
+    /**
+     * Indicates that there was an error parsing the response the query.
+     * The cause of this error is available via getCause() and is a {@link ParseException}.
+     */
+    public static final int ERROR_PARSE = 0;
+    /**
+     * Indicates that there was an error sending the query.
+     * The cause of this error is available via getCause() and is an ErrnoException.
+     */
+    public static final int ERROR_SYSTEM = 1;
+
+    private static final int NETID_UNSET = 0;
+
+    private static final DnsResolver sInstance = new DnsResolver();
+
+    /**
+     * Get instance for DnsResolver
+     */
+    public static @NonNull DnsResolver getInstance() {
+        return sInstance;
+    }
+
+    private DnsResolver() {}
+
+    /**
+     * Base interface for answer callbacks
+     *
+     * @param <T> The type of the answer
+     */
+    public interface Callback<T> {
+        /**
+         * Success response to
+         * {@link android.net.DnsResolver#query query()} or
+         * {@link android.net.DnsResolver#rawQuery rawQuery()}.
+         *
+         * Invoked when the answer to a query was successfully parsed.
+         *
+         * @param answer <T> answer to the query.
+         * @param rcode The response code in the DNS response.
+         *
+         * {@see android.net.DnsResolver#query query()}
+         */
+        void onAnswer(@NonNull T answer, int rcode);
+        /**
+         * Error response to
+         * {@link android.net.DnsResolver#query query()} or
+         * {@link android.net.DnsResolver#rawQuery rawQuery()}.
+         *
+         * Invoked when there is no valid answer to
+         * {@link android.net.DnsResolver#query query()}
+         * {@link android.net.DnsResolver#rawQuery rawQuery()}.
+         *
+         * @param error a {@link DnsException} object with additional
+         *    detail regarding the failure
+         */
+        void onError(@NonNull DnsException error);
+    }
+
+    /**
+     * Class to represent DNS error
+     */
+    public static class DnsException extends Exception {
+       /**
+        * DNS error code as one of the ERROR_* constants
+        */
+        @DnsError public final int code;
+
+        DnsException(@DnsError int code, @Nullable Throwable cause) {
+            super(cause);
+            this.code = code;
+        }
+    }
+
+    /**
+     * Send a raw DNS query.
+     * The answer will be provided asynchronously through the provided {@link Callback}.
+     *
+     * @param network {@link Network} specifying which network to query on.
+     *         {@code null} for query on default network.
+     * @param query blob message to query
+     * @param flags flags as a combination of the FLAGS_* constants
+     * @param executor The {@link Executor} that the callback should be executed on.
+     * @param cancellationSignal used by the caller to signal if the query should be
+     *    cancelled. May be {@code null}.
+     * @param callback a {@link Callback} which will be called to notify the caller
+     *    of the result of dns query.
+     */
+    public void rawQuery(@Nullable Network network, @NonNull byte[] query, @QueryFlag int flags,
+            @NonNull @CallbackExecutor Executor executor,
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull Callback<? super byte[]> callback) {
+        if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+            return;
+        }
+        final Object lock = new Object();
+        final FileDescriptor queryfd;
+        try {
+            queryfd = resNetworkSend((network != null)
+                    ? network.getNetIdForResolv() : NETID_UNSET, query, query.length, flags);
+        } catch (ErrnoException e) {
+            executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
+            return;
+        }
+
+        synchronized (lock)  {
+            registerFDListener(executor, queryfd, callback, cancellationSignal, lock);
+            if (cancellationSignal == null) return;
+            addCancellationSignal(cancellationSignal, queryfd, lock);
+        }
+    }
+
+    /**
+     * Send a DNS query with the specified name, class and query type.
+     * The answer will be provided asynchronously through the provided {@link Callback}.
+     *
+     * @param network {@link Network} specifying which network to query on.
+     *         {@code null} for query on default network.
+     * @param domain domain name to query
+     * @param nsClass dns class as one of the CLASS_* constants
+     * @param nsType dns resource record (RR) type as one of the TYPE_* constants
+     * @param flags flags as a combination of the FLAGS_* constants
+     * @param executor The {@link Executor} that the callback should be executed on.
+     * @param cancellationSignal used by the caller to signal if the query should be
+     *    cancelled. May be {@code null}.
+     * @param callback a {@link Callback} which will be called to notify the caller
+     *    of the result of dns query.
+     */
+    public void rawQuery(@Nullable Network network, @NonNull String domain,
+            @QueryClass int nsClass, @QueryType int nsType, @QueryFlag int flags,
+            @NonNull @CallbackExecutor Executor executor,
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull Callback<? super byte[]> callback) {
+        if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+            return;
+        }
+        final Object lock = new Object();
+        final FileDescriptor queryfd;
+        try {
+            queryfd = resNetworkQuery((network != null)
+                    ? network.getNetIdForResolv() : NETID_UNSET, domain, nsClass, nsType, flags);
+        } catch (ErrnoException e) {
+            executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
+            return;
+        }
+        synchronized (lock)  {
+            registerFDListener(executor, queryfd, callback, cancellationSignal, lock);
+            if (cancellationSignal == null) return;
+            addCancellationSignal(cancellationSignal, queryfd, lock);
+        }
+    }
+
+    private class InetAddressAnswerAccumulator implements Callback<byte[]> {
+        private final List<InetAddress> mAllAnswers;
+        private final Network mNetwork;
+        private int mRcode;
+        private DnsException mDnsException;
+        private final Callback<? super List<InetAddress>> mUserCallback;
+        private final int mTargetAnswerCount;
+        private int mReceivedAnswerCount = 0;
+
+        InetAddressAnswerAccumulator(@NonNull Network network, int size,
+                @NonNull Callback<? super List<InetAddress>> callback) {
+            mNetwork = network;
+            mTargetAnswerCount = size;
+            mAllAnswers = new ArrayList<>();
+            mUserCallback = callback;
+        }
+
+        private boolean maybeReportError() {
+            if (mRcode != 0) {
+                mUserCallback.onAnswer(mAllAnswers, mRcode);
+                return true;
+            }
+            if (mDnsException != null) {
+                mUserCallback.onError(mDnsException);
+                return true;
+            }
+            return false;
+        }
+
+        private void maybeReportAnswer() {
+            if (++mReceivedAnswerCount != mTargetAnswerCount) return;
+            if (mAllAnswers.isEmpty() && maybeReportError()) return;
+            mUserCallback.onAnswer(rfc6724Sort(mNetwork, mAllAnswers), mRcode);
+        }
+
+        @Override
+        public void onAnswer(@NonNull byte[] answer, int rcode) {
+            // If at least one query succeeded, return an rcode of 0.
+            // Otherwise, arbitrarily return the first rcode received.
+            if (mReceivedAnswerCount == 0 || rcode == 0) {
+                mRcode = rcode;
+            }
+            try {
+                mAllAnswers.addAll(new DnsAddressAnswer(answer).getAddresses());
+            } catch (DnsPacket.ParseException e) {
+                // Convert the com.android.net.module.util.DnsPacket.ParseException to an
+                // android.net.ParseException. This is the type that was used in Q and is implied
+                // by the public documentation of ERROR_PARSE.
+                //
+                // DnsPacket cannot throw android.net.ParseException directly because it's @hide.
+                ParseException pe = new ParseException(e.reason, e.getCause());
+                pe.setStackTrace(e.getStackTrace());
+                mDnsException = new DnsException(ERROR_PARSE, pe);
+            }
+            maybeReportAnswer();
+        }
+
+        @Override
+        public void onError(@NonNull DnsException error) {
+            mDnsException = error;
+            maybeReportAnswer();
+        }
+    }
+
+    /**
+     * Send a DNS query with the specified name on a network with both IPv4 and IPv6,
+     * get back a set of InetAddresses with rfc6724 sorting style asynchronously.
+     *
+     * This method will examine the connection ability on given network, and query IPv4
+     * and IPv6 if connection is available.
+     *
+     * If at least one query succeeded with valid answer, rcode will be 0
+     *
+     * The answer will be provided asynchronously through the provided {@link Callback}.
+     *
+     * @param network {@link Network} specifying which network to query on.
+     *         {@code null} for query on default network.
+     * @param domain domain name to query
+     * @param flags flags as a combination of the FLAGS_* constants
+     * @param executor The {@link Executor} that the callback should be executed on.
+     * @param cancellationSignal used by the caller to signal if the query should be
+     *    cancelled. May be {@code null}.
+     * @param callback a {@link Callback} which will be called to notify the
+     *    caller of the result of dns query.
+     */
+    public void query(@Nullable Network network, @NonNull String domain, @QueryFlag int flags,
+            @NonNull @CallbackExecutor Executor executor,
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull Callback<? super List<InetAddress>> callback) {
+        if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+            return;
+        }
+        final Object lock = new Object();
+        final Network queryNetwork;
+        try {
+            queryNetwork = (network != null) ? network : getDnsNetwork();
+        } catch (ErrnoException e) {
+            executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
+            return;
+        }
+        final boolean queryIpv6 = haveIpv6(queryNetwork);
+        final boolean queryIpv4 = haveIpv4(queryNetwork);
+
+        // This can only happen if queryIpv4 and queryIpv6 are both false.
+        // This almost certainly means that queryNetwork does not exist or no longer exists.
+        if (!queryIpv6 && !queryIpv4) {
+            executor.execute(() -> callback.onError(
+                    new DnsException(ERROR_SYSTEM, new ErrnoException("resNetworkQuery", ENONET))));
+            return;
+        }
+
+        final FileDescriptor v4fd;
+        final FileDescriptor v6fd;
+
+        int queryCount = 0;
+
+        if (queryIpv6) {
+            try {
+                v6fd = resNetworkQuery(queryNetwork.getNetIdForResolv(), domain, CLASS_IN,
+                        TYPE_AAAA, flags);
+            } catch (ErrnoException e) {
+                executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
+                return;
+            }
+            queryCount++;
+        } else v6fd = null;
+
+        // Avoiding gateways drop packets if queries are sent too close together
+        try {
+            Thread.sleep(SLEEP_TIME_MS);
+        } catch (InterruptedException ex) {
+            Thread.currentThread().interrupt();
+        }
+
+        if (queryIpv4) {
+            try {
+                v4fd = resNetworkQuery(queryNetwork.getNetIdForResolv(), domain, CLASS_IN, TYPE_A,
+                        flags);
+            } catch (ErrnoException e) {
+                if (queryIpv6) resNetworkCancel(v6fd);  // Closes fd, marks it invalid.
+                executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
+                return;
+            }
+            queryCount++;
+        } else v4fd = null;
+
+        final InetAddressAnswerAccumulator accumulator =
+                new InetAddressAnswerAccumulator(queryNetwork, queryCount, callback);
+
+        synchronized (lock)  {
+            if (queryIpv6) {
+                registerFDListener(executor, v6fd, accumulator, cancellationSignal, lock);
+            }
+            if (queryIpv4) {
+                registerFDListener(executor, v4fd, accumulator, cancellationSignal, lock);
+            }
+            if (cancellationSignal == null) return;
+            cancellationSignal.setOnCancelListener(() -> {
+                synchronized (lock)  {
+                    if (queryIpv4) cancelQuery(v4fd);
+                    if (queryIpv6) cancelQuery(v6fd);
+                }
+            });
+        }
+    }
+
+    /**
+     * Send a DNS query with the specified name and query type, get back a set of
+     * InetAddresses with rfc6724 sorting style asynchronously.
+     *
+     * The answer will be provided asynchronously through the provided {@link Callback}.
+     *
+     * @param network {@link Network} specifying which network to query on.
+     *         {@code null} for query on default network.
+     * @param domain domain name to query
+     * @param nsType dns resource record (RR) type as one of the TYPE_* constants
+     * @param flags flags as a combination of the FLAGS_* constants
+     * @param executor The {@link Executor} that the callback should be executed on.
+     * @param cancellationSignal used by the caller to signal if the query should be
+     *    cancelled. May be {@code null}.
+     * @param callback a {@link Callback} which will be called to notify the caller
+     *    of the result of dns query.
+     */
+    public void query(@Nullable Network network, @NonNull String domain,
+            @QueryType int nsType, @QueryFlag int flags,
+            @NonNull @CallbackExecutor Executor executor,
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull Callback<? super List<InetAddress>> callback) {
+        if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+            return;
+        }
+        final Object lock = new Object();
+        final FileDescriptor queryfd;
+        final Network queryNetwork;
+        try {
+            queryNetwork = (network != null) ? network : getDnsNetwork();
+            queryfd = resNetworkQuery(queryNetwork.getNetIdForResolv(), domain, CLASS_IN, nsType,
+                    flags);
+        } catch (ErrnoException e) {
+            executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
+            return;
+        }
+        final InetAddressAnswerAccumulator accumulator =
+                new InetAddressAnswerAccumulator(queryNetwork, 1, callback);
+        synchronized (lock)  {
+            registerFDListener(executor, queryfd, accumulator, cancellationSignal, lock);
+            if (cancellationSignal == null) return;
+            addCancellationSignal(cancellationSignal, queryfd, lock);
+        }
+    }
+
+    /**
+     * Class to retrieve DNS response
+     *
+     * @hide
+     */
+    public static final class DnsResponse {
+        public final @NonNull byte[] answerbuf;
+        public final int rcode;
+        public DnsResponse(@NonNull byte[] answerbuf, int rcode) {
+            this.answerbuf = answerbuf;
+            this.rcode = rcode;
+        }
+    }
+
+    private void registerFDListener(@NonNull Executor executor,
+            @NonNull FileDescriptor queryfd, @NonNull Callback<? super byte[]> answerCallback,
+            @Nullable CancellationSignal cancellationSignal, @NonNull Object lock) {
+        final MessageQueue mainThreadMessageQueue = Looper.getMainLooper().getQueue();
+        mainThreadMessageQueue.addOnFileDescriptorEventListener(
+                queryfd,
+                FD_EVENTS,
+                (fd, events) -> {
+                    // b/134310704
+                    // Unregister fd event listener before resNetworkResult is called to prevent
+                    // race condition caused by fd reused.
+                    // For example when querying v4 and v6, it's possible that the first query ends
+                    // and the fd is closed before the second request starts, which might return
+                    // the same fd for the second request. By that time, the looper must have
+                    // unregistered the fd, otherwise another event listener can't be registered.
+                    mainThreadMessageQueue.removeOnFileDescriptorEventListener(fd);
+
+                    executor.execute(() -> {
+                        DnsResponse resp = null;
+                        ErrnoException exception = null;
+                        synchronized (lock) {
+                            if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+                                return;
+                            }
+                            try {
+                                resp = resNetworkResult(fd);  // Closes fd, marks it invalid.
+                            } catch (ErrnoException e) {
+                                Log.w(TAG, "resNetworkResult:" + e.toString());
+                                exception = e;
+                            }
+                        }
+                        if (exception != null) {
+                            answerCallback.onError(new DnsException(ERROR_SYSTEM, exception));
+                            return;
+                        }
+                        answerCallback.onAnswer(resp.answerbuf, resp.rcode);
+                    });
+
+                    // The file descriptor has already been unregistered, so it does not really
+                    // matter what is returned here. In spirit 0 (meaning "unregister this FD")
+                    // is still the closest to what the looper needs to do. When returning 0,
+                    // Looper knows to ignore the fd if it has already been unregistered.
+                    return 0;
+                });
+    }
+
+    private void cancelQuery(@NonNull FileDescriptor queryfd) {
+        if (!queryfd.valid()) return;
+        Looper.getMainLooper().getQueue().removeOnFileDescriptorEventListener(queryfd);
+        resNetworkCancel(queryfd);  // Closes fd, marks it invalid.
+    }
+
+    private void addCancellationSignal(@NonNull CancellationSignal cancellationSignal,
+            @NonNull FileDescriptor queryfd, @NonNull Object lock) {
+        cancellationSignal.setOnCancelListener(() -> {
+            synchronized (lock)  {
+                cancelQuery(queryfd);
+            }
+        });
+    }
+
+    private static class DnsAddressAnswer extends DnsPacket {
+        private static final String TAG = "DnsResolver.DnsAddressAnswer";
+        private static final boolean DBG = false;
+
+        private final int mQueryType;
+
+        DnsAddressAnswer(@NonNull byte[] data) throws ParseException {
+            super(data);
+            if ((mHeader.flags & (1 << 15)) == 0) {
+                throw new ParseException("Not an answer packet");
+            }
+            if (mHeader.getRecordCount(QDSECTION) == 0) {
+                throw new ParseException("No question found");
+            }
+            // Expect only one question in question section.
+            mQueryType = mRecords[QDSECTION].get(0).nsType;
+        }
+
+        public @NonNull List<InetAddress> getAddresses() {
+            final List<InetAddress> results = new ArrayList<InetAddress>();
+            if (mHeader.getRecordCount(ANSECTION) == 0) return results;
+
+            for (final DnsRecord ansSec : mRecords[ANSECTION]) {
+                // Only support A and AAAA, also ignore answers if query type != answer type.
+                int nsType = ansSec.nsType;
+                if (nsType != mQueryType || (nsType != TYPE_A && nsType != TYPE_AAAA)) {
+                    continue;
+                }
+                try {
+                    results.add(InetAddress.getByAddress(ansSec.getRR()));
+                } catch (UnknownHostException e) {
+                    if (DBG) {
+                        Log.w(TAG, "rr to address fail");
+                    }
+                }
+            }
+            return results;
+        }
+    }
+
+}
diff --git a/framework/src/android/net/DnsResolverServiceManager.java b/framework/src/android/net/DnsResolverServiceManager.java
new file mode 100644
index 0000000..79009e8
--- /dev/null
+++ b/framework/src/android/net/DnsResolverServiceManager.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.os.IBinder;
+
+/**
+ * Provides a way to obtain the DnsResolver binder objects.
+ *
+ * @hide
+ */
+public class DnsResolverServiceManager {
+    /** Service name for the DNS resolver. Keep in sync with DnsResolverService.h */
+    public static final String DNS_RESOLVER_SERVICE = "dnsresolver";
+
+    private final IBinder mResolver;
+
+    DnsResolverServiceManager(IBinder resolver) {
+        mResolver = resolver;
+    }
+
+    /**
+     * Get an {@link IBinder} representing the DnsResolver stable AIDL interface
+     *
+     * @return {@link android.net.IDnsResolver} IBinder.
+     */
+    @NonNull
+    public IBinder getService() {
+        return mResolver;
+    }
+}
diff --git a/framework/src/android/net/ICaptivePortal.aidl b/framework/src/android/net/ICaptivePortal.aidl
new file mode 100644
index 0000000..e35f8d4
--- /dev/null
+++ b/framework/src/android/net/ICaptivePortal.aidl
@@ -0,0 +1,26 @@
+/**
+ * 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;
+
+/**
+ * Interface to inform NetworkMonitor of decisions of app handling captive portal.
+ * @hide
+ */
+oneway interface ICaptivePortal {
+    void appRequest(int request);
+    void appResponse(int response);
+}
diff --git a/framework/src/android/net/IConnectivityDiagnosticsCallback.aidl b/framework/src/android/net/IConnectivityDiagnosticsCallback.aidl
new file mode 100644
index 0000000..82b64a9
--- /dev/null
+++ b/framework/src/android/net/IConnectivityDiagnosticsCallback.aidl
@@ -0,0 +1,28 @@
+/**
+ *
+ * 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;
+
+import android.net.ConnectivityDiagnosticsManager;
+import android.net.Network;
+
+/** @hide */
+oneway interface IConnectivityDiagnosticsCallback {
+    void onConnectivityReportAvailable(in ConnectivityDiagnosticsManager.ConnectivityReport report);
+    void onDataStallSuspected(in ConnectivityDiagnosticsManager.DataStallReport report);
+    void onNetworkConnectivityReported(in Network n, boolean hasConnectivity);
+}
\ No newline at end of file
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
new file mode 100644
index 0000000..c434bbc
--- /dev/null
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -0,0 +1,229 @@
+/**
+ * Copyright (c) 2008, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.app.PendingIntent;
+import android.net.ConnectionInfo;
+import android.net.ConnectivityDiagnosticsManager;
+import android.net.IConnectivityDiagnosticsCallback;
+import android.net.INetworkAgent;
+import android.net.IOnCompleteListener;
+import android.net.INetworkActivityListener;
+import android.net.INetworkOfferCallback;
+import android.net.IQosCallback;
+import android.net.ISocketKeepaliveCallback;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.net.NetworkState;
+import android.net.NetworkStateSnapshot;
+import android.net.OemNetworkPreferences;
+import android.net.ProxyInfo;
+import android.net.UidRange;
+import android.net.QosSocketInfo;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Messenger;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.ResultReceiver;
+import android.os.UserHandle;
+
+/**
+ * Interface that answers queries about, and allows changing, the
+ * state of network connectivity.
+ */
+/** {@hide} */
+interface IConnectivityManager
+{
+    Network getActiveNetwork();
+    Network getActiveNetworkForUid(int uid, boolean ignoreBlocked);
+    @UnsupportedAppUsage
+    NetworkInfo getActiveNetworkInfo();
+    NetworkInfo getActiveNetworkInfoForUid(int uid, boolean ignoreBlocked);
+    @UnsupportedAppUsage(maxTargetSdk = 28)
+    NetworkInfo getNetworkInfo(int networkType);
+    NetworkInfo getNetworkInfoForUid(in Network network, int uid, boolean ignoreBlocked);
+    @UnsupportedAppUsage
+    NetworkInfo[] getAllNetworkInfo();
+    Network getNetworkForType(int networkType);
+    Network[] getAllNetworks();
+    NetworkCapabilities[] getDefaultNetworkCapabilitiesForUser(
+            int userId, String callingPackageName, String callingAttributionTag);
+
+    boolean isNetworkSupported(int networkType);
+
+    @UnsupportedAppUsage
+    LinkProperties getActiveLinkProperties();
+    LinkProperties getLinkPropertiesForType(int networkType);
+    LinkProperties getLinkProperties(in Network network);
+
+    NetworkCapabilities getNetworkCapabilities(in Network network, String callingPackageName,
+            String callingAttributionTag);
+
+    @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
+    NetworkState[] getAllNetworkState();
+
+    List<NetworkStateSnapshot> getAllNetworkStateSnapshots();
+
+    boolean isActiveNetworkMetered();
+
+    boolean requestRouteToHostAddress(int networkType, in byte[] hostAddress,
+            String callingPackageName, String callingAttributionTag);
+
+    @UnsupportedAppUsage(maxTargetSdk = 29,
+            publicAlternatives = "Use {@code TetheringManager#getLastTetherError} as alternative")
+    int getLastTetherError(String iface);
+
+    @UnsupportedAppUsage(maxTargetSdk = 29,
+            publicAlternatives = "Use {@code TetheringManager#getTetherableIfaces} as alternative")
+    String[] getTetherableIfaces();
+
+    @UnsupportedAppUsage(maxTargetSdk = 29,
+            publicAlternatives = "Use {@code TetheringManager#getTetheredIfaces} as alternative")
+    String[] getTetheredIfaces();
+
+    @UnsupportedAppUsage(maxTargetSdk = 29,
+            publicAlternatives = "Use {@code TetheringManager#getTetheringErroredIfaces} "
+            + "as Alternative")
+    String[] getTetheringErroredIfaces();
+
+    @UnsupportedAppUsage(maxTargetSdk = 29,
+            publicAlternatives = "Use {@code TetheringManager#getTetherableUsbRegexs} as alternative")
+    String[] getTetherableUsbRegexs();
+
+    @UnsupportedAppUsage(maxTargetSdk = 29,
+            publicAlternatives = "Use {@code TetheringManager#getTetherableWifiRegexs} as alternative")
+    String[] getTetherableWifiRegexs();
+
+    @UnsupportedAppUsage(maxTargetSdk = 28)
+    void reportInetCondition(int networkType, int percentage);
+
+    void reportNetworkConnectivity(in Network network, boolean hasConnectivity);
+
+    ProxyInfo getGlobalProxy();
+
+    void setGlobalProxy(in ProxyInfo p);
+
+    ProxyInfo getProxyForNetwork(in Network nework);
+
+    void setRequireVpnForUids(boolean requireVpn, in UidRange[] ranges);
+    void setLegacyLockdownVpnEnabled(boolean enabled);
+
+    void setProvisioningNotificationVisible(boolean visible, int networkType, in String action);
+
+    void setAirplaneMode(boolean enable);
+
+    boolean requestBandwidthUpdate(in Network network);
+
+    int registerNetworkProvider(in Messenger messenger, in String name);
+    void unregisterNetworkProvider(in Messenger messenger);
+
+    void declareNetworkRequestUnfulfillable(in NetworkRequest request);
+
+    Network registerNetworkAgent(in INetworkAgent na, in NetworkInfo ni, in LinkProperties lp,
+            in NetworkCapabilities nc, in NetworkScore score, in NetworkAgentConfig config,
+            in int factorySerialNumber);
+
+    NetworkRequest requestNetwork(int uid, in NetworkCapabilities networkCapabilities, int reqType,
+            in Messenger messenger, int timeoutSec, in IBinder binder, int legacy,
+            int callbackFlags, String callingPackageName, String callingAttributionTag);
+
+    NetworkRequest pendingRequestForNetwork(in NetworkCapabilities networkCapabilities,
+            in PendingIntent operation, String callingPackageName, String callingAttributionTag);
+
+    void releasePendingNetworkRequest(in PendingIntent operation);
+
+    NetworkRequest listenForNetwork(in NetworkCapabilities networkCapabilities,
+            in Messenger messenger, in IBinder binder, int callbackFlags, String callingPackageName,
+            String callingAttributionTag);
+
+    void pendingListenForNetwork(in NetworkCapabilities networkCapabilities,
+            in PendingIntent operation, String callingPackageName,
+            String callingAttributionTag);
+
+    void releaseNetworkRequest(in NetworkRequest networkRequest);
+
+    void setAcceptUnvalidated(in Network network, boolean accept, boolean always);
+    void setAcceptPartialConnectivity(in Network network, boolean accept, boolean always);
+    void setAvoidUnvalidated(in Network network);
+    void startCaptivePortalApp(in Network network);
+    void startCaptivePortalAppInternal(in Network network, in Bundle appExtras);
+
+    boolean shouldAvoidBadWifi();
+    int getMultipathPreference(in Network Network);
+
+    NetworkRequest getDefaultRequest();
+
+    int getRestoreDefaultNetworkDelay(int networkType);
+
+    void factoryReset();
+
+    void startNattKeepalive(in Network network, int intervalSeconds,
+            in ISocketKeepaliveCallback cb, String srcAddr, int srcPort, String dstAddr);
+
+    void startNattKeepaliveWithFd(in Network network, in ParcelFileDescriptor pfd, int resourceId,
+            int intervalSeconds, in ISocketKeepaliveCallback cb, String srcAddr,
+            String dstAddr);
+
+    void startTcpKeepalive(in Network network, in ParcelFileDescriptor pfd, int intervalSeconds,
+            in ISocketKeepaliveCallback cb);
+
+    void stopKeepalive(in Network network, int slot);
+
+    String getCaptivePortalServerUrl();
+
+    byte[] getNetworkWatchlistConfigHash();
+
+    int getConnectionOwnerUid(in ConnectionInfo connectionInfo);
+
+    void registerConnectivityDiagnosticsCallback(in IConnectivityDiagnosticsCallback callback,
+            in NetworkRequest request, String callingPackageName);
+    void unregisterConnectivityDiagnosticsCallback(in IConnectivityDiagnosticsCallback callback);
+
+    IBinder startOrGetTestNetworkService();
+
+    void simulateDataStall(int detectionMethod, long timestampMillis, in Network network,
+                in PersistableBundle extras);
+
+    void systemReady();
+
+    void registerNetworkActivityListener(in INetworkActivityListener l);
+
+    void unregisterNetworkActivityListener(in INetworkActivityListener l);
+
+    boolean isDefaultNetworkActive();
+
+    void registerQosSocketCallback(in QosSocketInfo socketInfo, in IQosCallback callback);
+    void unregisterQosCallback(in IQosCallback callback);
+
+    void setOemNetworkPreference(in OemNetworkPreferences preference,
+            in IOnCompleteListener listener);
+
+    void setProfileNetworkPreference(in UserHandle profile, int preference,
+            in IOnCompleteListener listener);
+
+    int getRestrictBackgroundStatusByCaller();
+
+    void offerNetwork(int providerId, in NetworkScore score,
+            in NetworkCapabilities caps, in INetworkOfferCallback callback);
+    void unofferNetwork(in INetworkOfferCallback callback);
+}
diff --git a/framework/src/android/net/INetworkActivityListener.aidl b/framework/src/android/net/INetworkActivityListener.aidl
new file mode 100644
index 0000000..79687dd
--- /dev/null
+++ b/framework/src/android/net/INetworkActivityListener.aidl
@@ -0,0 +1,24 @@
+/* Copyright 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.
+*/
+
+package android.net;
+
+/**
+ * @hide
+ */
+oneway interface INetworkActivityListener
+{
+    void onNetworkActive();
+}
diff --git a/framework/src/android/net/INetworkAgent.aidl b/framework/src/android/net/INetworkAgent.aidl
new file mode 100644
index 0000000..d941d4b
--- /dev/null
+++ b/framework/src/android/net/INetworkAgent.aidl
@@ -0,0 +1,51 @@
+/**
+ * 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 perNmissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.net.NattKeepalivePacketData;
+import android.net.QosFilterParcelable;
+import android.net.TcpKeepalivePacketData;
+
+import android.net.INetworkAgentRegistry;
+
+/**
+ * Interface to notify NetworkAgent of connectivity events.
+ * @hide
+ */
+oneway interface INetworkAgent {
+    void onRegistered(in INetworkAgentRegistry registry);
+    void onDisconnected();
+    void onBandwidthUpdateRequested();
+    void onValidationStatusChanged(int validationStatus,
+            in @nullable String captivePortalUrl);
+    void onSaveAcceptUnvalidated(boolean acceptUnvalidated);
+    void onStartNattSocketKeepalive(int slot, int intervalDurationMs,
+        in NattKeepalivePacketData packetData);
+    void onStartTcpSocketKeepalive(int slot, int intervalDurationMs,
+        in TcpKeepalivePacketData packetData);
+    void onStopSocketKeepalive(int slot);
+    void onSignalStrengthThresholdsUpdated(in int[] thresholds);
+    void onPreventAutomaticReconnect();
+    void onAddNattKeepalivePacketFilter(int slot,
+        in NattKeepalivePacketData packetData);
+    void onAddTcpKeepalivePacketFilter(int slot,
+        in TcpKeepalivePacketData packetData);
+    void onRemoveKeepalivePacketFilter(int slot);
+    void onQosFilterCallbackRegistered(int qosCallbackId, in QosFilterParcelable filterParcel);
+    void onQosCallbackUnregistered(int qosCallbackId);
+    void onNetworkCreated();
+    void onNetworkDestroyed();
+}
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl
new file mode 100644
index 0000000..9a58add
--- /dev/null
+++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -0,0 +1,46 @@
+/**
+ * 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 perNmissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkScore;
+import android.net.QosSession;
+import android.telephony.data.EpsBearerQosSessionAttributes;
+import android.telephony.data.NrQosSessionAttributes;
+
+/**
+ * Interface for NetworkAgents to send network properties.
+ * @hide
+ */
+oneway interface INetworkAgentRegistry {
+    void sendNetworkCapabilities(in NetworkCapabilities nc);
+    void sendLinkProperties(in LinkProperties lp);
+    // TODO: consider replacing this by "markConnected()" and removing
+    void sendNetworkInfo(in NetworkInfo info);
+    void sendScore(in NetworkScore score);
+    void sendExplicitlySelected(boolean explicitlySelected, boolean acceptPartial);
+    void sendSocketKeepaliveEvent(int slot, int reason);
+    void sendUnderlyingNetworks(in @nullable List<Network> networks);
+    void sendEpsQosSessionAvailable(int callbackId, in QosSession session, in EpsBearerQosSessionAttributes attributes);
+    void sendNrQosSessionAvailable(int callbackId, in QosSession session, in NrQosSessionAttributes attributes);
+    void sendQosSessionLost(int qosCallbackId, in QosSession session);
+    void sendQosCallbackError(int qosCallbackId, int exceptionType);
+    void sendTeardownDelayMs(int teardownDelayMs);
+    void sendLingerDuration(int durationMs);
+}
diff --git a/framework/src/android/net/INetworkOfferCallback.aidl b/framework/src/android/net/INetworkOfferCallback.aidl
new file mode 100644
index 0000000..ecfba21
--- /dev/null
+++ b/framework/src/android/net/INetworkOfferCallback.aidl
@@ -0,0 +1,61 @@
+/*
+ * 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.NetworkRequest;
+
+/**
+ * A callback registered with connectivity by network providers together with
+ * a NetworkOffer.
+ *
+ * When the network for this offer is needed to satisfy some application or
+ * system component, connectivity will call onNetworkNeeded on this callback.
+ * When this happens, the provider should try and bring up the network.
+ *
+ * When the network for this offer is no longer needed, for example because
+ * the application has withdrawn the request or if the request is being
+ * satisfied by a network that this offer will never be able to beat,
+ * connectivity calls onNetworkUnneeded. When this happens, the provider
+ * should stop trying to bring up the network, or tear it down if it has
+ * already been brought up.
+ *
+ * When NetworkProvider#offerNetwork is called, the provider can expect to
+ * immediately receive all requests that can be fulfilled by that offer and
+ * are not already satisfied by a better network. It is possible no such
+ * request is currently outstanding, because no requests have been made that
+ * can be satisfied by this offer, or because all such requests are already
+ * satisfied by a better network.
+ * onNetworkNeeded can be called at any time after registration and until the
+ * offer is withdrawn with NetworkProvider#unofferNetwork is called. This
+ * typically happens when a new network request is filed by an application,
+ * or when the network satisfying a request disconnects and this offer now
+ * stands a chance to supply the best network for it.
+ *
+ * @hide
+ */
+oneway interface INetworkOfferCallback {
+    /**
+     * Called when a network for this offer is needed to fulfill this request.
+     * @param networkRequest the request to satisfy
+     */
+    void onNetworkNeeded(in NetworkRequest networkRequest);
+
+    /**
+     * Informs the registrant that the offer is no longer valuable to fulfill this request.
+     */
+    void onNetworkUnneeded(in NetworkRequest networkRequest);
+}
diff --git a/framework/src/android/net/IOnCompleteListener.aidl b/framework/src/android/net/IOnCompleteListener.aidl
new file mode 100644
index 0000000..4bb89f6
--- /dev/null
+++ b/framework/src/android/net/IOnCompleteListener.aidl
@@ -0,0 +1,23 @@
+/**
+ *
+ * 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;
+
+/** @hide */
+oneway interface IOnCompleteListener {
+    void onComplete();
+}
diff --git a/framework/src/android/net/IQosCallback.aidl b/framework/src/android/net/IQosCallback.aidl
new file mode 100644
index 0000000..c973541
--- /dev/null
+++ b/framework/src/android/net/IQosCallback.aidl
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+import android.os.Bundle;
+import android.net.QosSession;
+import android.telephony.data.EpsBearerQosSessionAttributes;
+import android.telephony.data.NrQosSessionAttributes;
+
+/**
+ * AIDL interface for QosCallback
+ *
+ * @hide
+ */
+oneway interface IQosCallback
+{
+     void onQosEpsBearerSessionAvailable(in QosSession session,
+        in EpsBearerQosSessionAttributes attributes);
+     void onNrQosSessionAvailable(in QosSession session,
+        in NrQosSessionAttributes attributes);
+     void onQosSessionLost(in QosSession session);
+     void onError(in int type);
+}
diff --git a/framework/src/android/net/ISocketKeepaliveCallback.aidl b/framework/src/android/net/ISocketKeepaliveCallback.aidl
new file mode 100644
index 0000000..020fbca
--- /dev/null
+++ b/framework/src/android/net/ISocketKeepaliveCallback.aidl
@@ -0,0 +1,34 @@
+/**
+ * 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;
+
+/**
+ * Callback to provide status changes of keepalive offload.
+ *
+ * @hide
+ */
+oneway interface ISocketKeepaliveCallback
+{
+    /** The keepalive was successfully started. */
+    void onStarted(int slot);
+    /** The keepalive was successfully stopped. */
+    void onStopped();
+    /** The keepalive was stopped because of an error. */
+    void onError(int error);
+    /** The keepalive on a TCP socket was stopped because the socket received data. */
+    void onDataReceived();
+}
diff --git a/framework/src/android/net/ITestNetworkManager.aidl b/framework/src/android/net/ITestNetworkManager.aidl
new file mode 100644
index 0000000..2a863ad
--- /dev/null
+++ b/framework/src/android/net/ITestNetworkManager.aidl
@@ -0,0 +1,39 @@
+/**
+ * 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.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.TestNetworkInterface;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Interface that allows for creation and management of test-only networks.
+ *
+ * @hide
+ */
+interface ITestNetworkManager
+{
+    TestNetworkInterface createTunInterface(in LinkAddress[] linkAddrs);
+    TestNetworkInterface createTapInterface();
+
+    void setupTestNetwork(in String iface, in LinkProperties lp, in boolean isMetered,
+            in int[] administratorUids, in IBinder binder);
+
+    void teardownTestNetwork(int netId);
+}
diff --git a/framework/src/android/net/InetAddressCompat.java b/framework/src/android/net/InetAddressCompat.java
new file mode 100644
index 0000000..6b7e75c
--- /dev/null
+++ b/framework/src/android/net/InetAddressCompat.java
@@ -0,0 +1,88 @@
+/*
+ * 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.util.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Compatibility utility for InetAddress core platform APIs.
+ *
+ * Connectivity has access to such APIs, but they are not part of the module_current stubs yet
+ * (only core_current). Most stable core platform APIs are included manually in the connectivity
+ * build rules, but because InetAddress is also part of the base java SDK that is earlier on the
+ * classpath, the extra core platform APIs are not seen.
+ *
+ * TODO (b/183097033): remove this utility as soon as core_current is part of module_current
+ * @hide
+ */
+public class InetAddressCompat {
+
+    /**
+     * @see InetAddress#clearDnsCache()
+     */
+    public static void clearDnsCache() {
+        try {
+            InetAddress.class.getMethod("clearDnsCache").invoke(null);
+        } catch (InvocationTargetException e) {
+            if (e.getCause() instanceof RuntimeException) {
+                throw (RuntimeException) e.getCause();
+            }
+            throw new IllegalStateException("Unknown InvocationTargetException", e.getCause());
+        } catch (IllegalAccessException | NoSuchMethodException e) {
+            Log.wtf(InetAddressCompat.class.getSimpleName(), "Error clearing DNS cache", e);
+        }
+    }
+
+    /**
+     * @see InetAddress#getAllByNameOnNet(String, int)
+     */
+    public static InetAddress[] getAllByNameOnNet(String host, int netId) throws
+            UnknownHostException {
+        return (InetAddress[]) callGetByNameMethod("getAllByNameOnNet", host, netId);
+    }
+
+    /**
+     * @see InetAddress#getByNameOnNet(String, int)
+     */
+    public static InetAddress getByNameOnNet(String host, int netId) throws
+            UnknownHostException {
+        return (InetAddress) callGetByNameMethod("getByNameOnNet", host, netId);
+    }
+
+    private static Object callGetByNameMethod(String method, String host, int netId)
+            throws UnknownHostException {
+        try {
+            return InetAddress.class.getMethod(method, String.class, int.class)
+                    .invoke(null, host, netId);
+        } catch (InvocationTargetException e) {
+            if (e.getCause() instanceof UnknownHostException) {
+                throw (UnknownHostException) e.getCause();
+            }
+            if (e.getCause() instanceof RuntimeException) {
+                throw (RuntimeException) e.getCause();
+            }
+            throw new IllegalStateException("Unknown InvocationTargetException", e.getCause());
+        } catch (IllegalAccessException | NoSuchMethodException e) {
+            Log.wtf(InetAddressCompat.class.getSimpleName(), "Error calling " + method, e);
+            throw new IllegalStateException("Error querying via " + method, e);
+        }
+    }
+}
diff --git a/framework/src/android/net/InetAddresses.java b/framework/src/android/net/InetAddresses.java
new file mode 100644
index 0000000..01b795e
--- /dev/null
+++ b/framework/src/android/net/InetAddresses.java
@@ -0,0 +1,65 @@
+/*
+ * 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.annotation.NonNull;
+
+import libcore.net.InetAddressUtils;
+
+import java.net.InetAddress;
+
+/**
+ * Utility methods for {@link InetAddress} implementations.
+ */
+public class InetAddresses {
+
+    private InetAddresses() {}
+
+    /**
+     * Checks to see if the {@code address} is a numeric address (such as {@code "192.0.2.1"} or
+     * {@code "2001:db8::1:2"}).
+     *
+     * <p>A numeric address is either an IPv4 address containing exactly 4 decimal numbers or an
+     * IPv6 numeric address. IPv4 addresses that consist of either hexadecimal or octal digits or
+     * do not have exactly 4 numbers are not treated as numeric.
+     *
+     * <p>This method will never do a DNS lookup.
+     *
+     * @param address the address to parse.
+     * @return true if the supplied address is numeric, false otherwise.
+     */
+    public static boolean isNumericAddress(@NonNull String address) {
+        return InetAddressUtils.isNumericAddress(address);
+    }
+
+    /**
+     * Returns an InetAddress corresponding to the given numeric address (such
+     * as {@code "192.168.0.1"} or {@code "2001:4860:800d::68"}).
+     *
+     * <p>See {@link #isNumericAddress(String)} (String)} for a definition as to what constitutes a
+     * numeric address.
+     *
+     * <p>This method will never do a DNS lookup.
+     *
+     * @param address the address to parse, must be numeric.
+     * @return an {@link InetAddress} instance corresponding to the address.
+     * @throws IllegalArgumentException if {@code address} is not a numeric address.
+     */
+    public static @NonNull InetAddress parseNumericAddress(@NonNull String address) {
+        return InetAddressUtils.parseNumericAddress(address);
+    }
+}
diff --git a/framework/src/android/net/InvalidPacketException.java b/framework/src/android/net/InvalidPacketException.java
new file mode 100644
index 0000000..1873d77
--- /dev/null
+++ b/framework/src/android/net/InvalidPacketException.java
@@ -0,0 +1,66 @@
+/*
+ * 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;
+
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Thrown when a packet is invalid.
+ * @hide
+ */
+@SystemApi
+public final class InvalidPacketException extends Exception {
+    private final int mError;
+
+    // Must match SocketKeepalive#ERROR_INVALID_IP_ADDRESS.
+    /** Invalid IP address. */
+    public static final int ERROR_INVALID_IP_ADDRESS = -21;
+
+    // Must match SocketKeepalive#ERROR_INVALID_PORT.
+    /** Invalid port number. */
+    public static final int ERROR_INVALID_PORT = -22;
+
+    // Must match SocketKeepalive#ERROR_INVALID_LENGTH.
+    /** Invalid packet length. */
+    public static final int ERROR_INVALID_LENGTH = -23;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "ERROR_" }, value = {
+        ERROR_INVALID_IP_ADDRESS,
+        ERROR_INVALID_PORT,
+        ERROR_INVALID_LENGTH
+    })
+    public @interface ErrorCode {}
+
+    /**
+     * This packet is invalid.
+     * See the error code for details.
+     */
+    public InvalidPacketException(@ErrorCode final int error) {
+        this.mError = error;
+    }
+
+    /** Get error code. */
+    public int getError() {
+        return mError;
+    }
+}
diff --git a/framework/src/android/net/IpConfiguration.java b/framework/src/android/net/IpConfiguration.java
new file mode 100644
index 0000000..d5f8b2e
--- /dev/null
+++ b/framework/src/android/net/IpConfiguration.java
@@ -0,0 +1,223 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A class representing a configured network.
+ * @hide
+ */
+@SystemApi
+public final class IpConfiguration implements Parcelable {
+    private static final String TAG = "IpConfiguration";
+
+    // This enum has been used by apps through reflection for many releases.
+    // Therefore they can't just be removed. Duplicating these constants to
+    // give an alternate SystemApi is a worse option than exposing them.
+    @SuppressLint("Enum")
+    public enum IpAssignment {
+        /* Use statically configured IP settings. Configuration can be accessed
+         * with staticIpConfiguration */
+        STATIC,
+        /* Use dynamically configured IP settings */
+        DHCP,
+        /* no IP details are assigned, this is used to indicate
+         * that any existing IP settings should be retained */
+        UNASSIGNED
+    }
+
+    /** @hide */
+    public IpAssignment ipAssignment;
+
+    /** @hide */
+    public StaticIpConfiguration staticIpConfiguration;
+
+    // This enum has been used by apps through reflection for many releases.
+    // Therefore they can't just be removed. Duplicating these constants to
+    // give an alternate SystemApi is a worse option than exposing them.
+    @SuppressLint("Enum")
+    public enum ProxySettings {
+        /* No proxy is to be used. Any existing proxy settings
+         * should be cleared. */
+        NONE,
+        /* Use statically configured proxy. Configuration can be accessed
+         * with httpProxy. */
+        STATIC,
+        /* no proxy details are assigned, this is used to indicate
+         * that any existing proxy settings should be retained */
+        UNASSIGNED,
+        /* Use a Pac based proxy.
+         */
+        PAC
+    }
+
+    /** @hide */
+    public ProxySettings proxySettings;
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public ProxyInfo httpProxy;
+
+    private void init(IpAssignment ipAssignment,
+                      ProxySettings proxySettings,
+                      StaticIpConfiguration staticIpConfiguration,
+                      ProxyInfo httpProxy) {
+        this.ipAssignment = ipAssignment;
+        this.proxySettings = proxySettings;
+        this.staticIpConfiguration = (staticIpConfiguration == null) ?
+                null : new StaticIpConfiguration(staticIpConfiguration);
+        this.httpProxy = (httpProxy == null) ?
+                null : new ProxyInfo(httpProxy);
+    }
+
+    public IpConfiguration() {
+        init(IpAssignment.UNASSIGNED, ProxySettings.UNASSIGNED, null, null);
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public IpConfiguration(IpAssignment ipAssignment,
+                           ProxySettings proxySettings,
+                           StaticIpConfiguration staticIpConfiguration,
+                           ProxyInfo httpProxy) {
+        init(ipAssignment, proxySettings, staticIpConfiguration, httpProxy);
+    }
+
+    public IpConfiguration(@NonNull IpConfiguration source) {
+        this();
+        if (source != null) {
+            init(source.ipAssignment, source.proxySettings,
+                 source.staticIpConfiguration, source.httpProxy);
+        }
+    }
+
+    public @NonNull IpAssignment getIpAssignment() {
+        return ipAssignment;
+    }
+
+    public void setIpAssignment(@NonNull IpAssignment ipAssignment) {
+        this.ipAssignment = ipAssignment;
+    }
+
+    public @Nullable StaticIpConfiguration getStaticIpConfiguration() {
+        return staticIpConfiguration;
+    }
+
+    public void setStaticIpConfiguration(@Nullable StaticIpConfiguration staticIpConfiguration) {
+        this.staticIpConfiguration = staticIpConfiguration;
+    }
+
+    public @NonNull ProxySettings getProxySettings() {
+        return proxySettings;
+    }
+
+    public void setProxySettings(@NonNull ProxySettings proxySettings) {
+        this.proxySettings = proxySettings;
+    }
+
+    public @Nullable ProxyInfo getHttpProxy() {
+        return httpProxy;
+    }
+
+    public void setHttpProxy(@Nullable ProxyInfo httpProxy) {
+        this.httpProxy = httpProxy;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sbuf = new StringBuilder();
+        sbuf.append("IP assignment: " + ipAssignment.toString());
+        sbuf.append("\n");
+        if (staticIpConfiguration != null) {
+            sbuf.append("Static configuration: " + staticIpConfiguration.toString());
+            sbuf.append("\n");
+        }
+        sbuf.append("Proxy settings: " + proxySettings.toString());
+        sbuf.append("\n");
+        if (httpProxy != null) {
+            sbuf.append("HTTP proxy: " + httpProxy.toString());
+            sbuf.append("\n");
+        }
+
+        return sbuf.toString();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (o == this) {
+            return true;
+        }
+
+        if (!(o instanceof IpConfiguration)) {
+            return false;
+        }
+
+        IpConfiguration other = (IpConfiguration) o;
+        return this.ipAssignment == other.ipAssignment &&
+                this.proxySettings == other.proxySettings &&
+                Objects.equals(this.staticIpConfiguration, other.staticIpConfiguration) &&
+                Objects.equals(this.httpProxy, other.httpProxy);
+    }
+
+    @Override
+    public int hashCode() {
+        return 13 + (staticIpConfiguration != null ? staticIpConfiguration.hashCode() : 0) +
+               17 * ipAssignment.ordinal() +
+               47 * proxySettings.ordinal() +
+               83 * httpProxy.hashCode();
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString(ipAssignment.name());
+        dest.writeString(proxySettings.name());
+        dest.writeParcelable(staticIpConfiguration, flags);
+        dest.writeParcelable(httpProxy, flags);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @NonNull Creator<IpConfiguration> CREATOR =
+        new Creator<IpConfiguration>() {
+            public IpConfiguration createFromParcel(Parcel in) {
+                IpConfiguration config = new IpConfiguration();
+                config.ipAssignment = IpAssignment.valueOf(in.readString());
+                config.proxySettings = ProxySettings.valueOf(in.readString());
+                config.staticIpConfiguration = in.readParcelable(null);
+                config.httpProxy = in.readParcelable(null);
+                return config;
+            }
+
+            public IpConfiguration[] newArray(int size) {
+                return new IpConfiguration[size];
+            }
+        };
+}
diff --git a/framework/src/android/net/IpPrefix.java b/framework/src/android/net/IpPrefix.java
new file mode 100644
index 0000000..bf4481a
--- /dev/null
+++ b/framework/src/android/net/IpPrefix.java
@@ -0,0 +1,303 @@
+/*
+ * 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.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+
+import com.android.net.module.util.NetUtils;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Comparator;
+
+/**
+ * This class represents an IP prefix, i.e., a contiguous block of IP addresses aligned on a
+ * power of two boundary (also known as an "IP subnet"). A prefix is specified by two pieces of
+ * information:
+ *
+ * <ul>
+ * <li>A starting IP address (IPv4 or IPv6). This is the first IP address of the prefix.
+ * <li>A prefix length. This specifies the length of the prefix by specifing the number of bits
+ *     in the IP address, starting from the most significant bit in network byte order, that
+ *     are constant for all addresses in the prefix.
+ * </ul>
+ *
+ * For example, the prefix <code>192.0.2.0/24</code> covers the 256 IPv4 addresses from
+ * <code>192.0.2.0</code> to <code>192.0.2.255</code>, inclusive, and the prefix
+ * <code>2001:db8:1:2</code>  covers the 2^64 IPv6 addresses from <code>2001:db8:1:2::</code> to
+ * <code>2001:db8:1:2:ffff:ffff:ffff:ffff</code>, inclusive.
+ *
+ * Objects of this class are immutable.
+ */
+public final class IpPrefix implements Parcelable {
+    private final byte[] address;  // network byte order
+    private final int prefixLength;
+
+    private void checkAndMaskAddressAndPrefixLength() {
+        if (address.length != 4 && address.length != 16) {
+            throw new IllegalArgumentException(
+                    "IpPrefix has " + address.length + " bytes which is neither 4 nor 16");
+        }
+        NetUtils.maskRawAddress(address, prefixLength);
+    }
+
+    /**
+     * Constructs a new {@code IpPrefix} from a byte array containing an IPv4 or IPv6 address in
+     * network byte order and a prefix length. Silently truncates the address to the prefix length,
+     * so for example {@code 192.0.2.1/24} is silently converted to {@code 192.0.2.0/24}.
+     *
+     * @param address the IP address. Must be non-null and exactly 4 or 16 bytes long.
+     * @param prefixLength the prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
+     *
+     * @hide
+     */
+    public IpPrefix(@NonNull byte[] address, @IntRange(from = 0, to = 128) int prefixLength) {
+        this.address = address.clone();
+        this.prefixLength = prefixLength;
+        checkAndMaskAddressAndPrefixLength();
+    }
+
+    /**
+     * Constructs a new {@code IpPrefix} from an IPv4 or IPv6 address and a prefix length. Silently
+     * truncates the address to the prefix length, so for example {@code 192.0.2.1/24} is silently
+     * converted to {@code 192.0.2.0/24}.
+     *
+     * @param address the IP address. Must be non-null.
+     * @param prefixLength the prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
+     * @hide
+     */
+    @SystemApi
+    public IpPrefix(@NonNull InetAddress address, @IntRange(from = 0, to = 128) int prefixLength) {
+        // We don't reuse the (byte[], int) constructor because it calls clone() on the byte array,
+        // which is unnecessary because getAddress() already returns a clone.
+        this.address = address.getAddress();
+        this.prefixLength = prefixLength;
+        checkAndMaskAddressAndPrefixLength();
+    }
+
+    /**
+     * Constructs a new IpPrefix from a string such as "192.0.2.1/24" or "2001:db8::1/64".
+     * Silently truncates the address to the prefix length, so for example {@code 192.0.2.1/24}
+     * is silently converted to {@code 192.0.2.0/24}.
+     *
+     * @param prefix the prefix to parse
+     *
+     * @hide
+     */
+    @SystemApi
+    public IpPrefix(@NonNull String prefix) {
+        // We don't reuse the (InetAddress, int) constructor because "error: call to this must be
+        // first statement in constructor". We could factor out setting the member variables to an
+        // init() method, but if we did, then we'd have to make the members non-final, or "error:
+        // cannot assign a value to final variable address". So we just duplicate the code here.
+        Pair<InetAddress, Integer> ipAndMask = NetworkUtils.legacyParseIpAndMask(prefix);
+        this.address = ipAndMask.first.getAddress();
+        this.prefixLength = ipAndMask.second;
+        checkAndMaskAddressAndPrefixLength();
+    }
+
+    /**
+     * Compares this {@code IpPrefix} object against the specified object in {@code obj}. Two
+     * objects are equal if they have the same startAddress and prefixLength.
+     *
+     * @param obj the object to be tested for equality.
+     * @return {@code true} if both objects are equal, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof IpPrefix)) {
+            return false;
+        }
+        IpPrefix that = (IpPrefix) obj;
+        return Arrays.equals(this.address, that.address) && this.prefixLength == that.prefixLength;
+    }
+
+    /**
+     * Gets the hashcode of the represented IP prefix.
+     *
+     * @return the appropriate hashcode value.
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(address) + 11 * prefixLength;
+    }
+
+    /**
+     * Returns a copy of the first IP address in the prefix. Modifying the returned object does not
+     * change this object's contents.
+     *
+     * @return the address in the form of a byte array.
+     */
+    public @NonNull InetAddress getAddress() {
+        try {
+            return InetAddress.getByAddress(address);
+        } catch (UnknownHostException e) {
+            // Cannot happen. InetAddress.getByAddress can only throw an exception if the byte
+            // array is the wrong length, but we check that in the constructor.
+            throw new IllegalArgumentException("Address is invalid");
+        }
+    }
+
+    /**
+     * Returns a copy of the IP address bytes in network order (the highest order byte is the zeroth
+     * element). Modifying the returned array does not change this object's contents.
+     *
+     * @return the address in the form of a byte array.
+     */
+    public @NonNull byte[] getRawAddress() {
+        return address.clone();
+    }
+
+    /**
+     * Returns the prefix length of this {@code IpPrefix}.
+     *
+     * @return the prefix length.
+     */
+    @IntRange(from = 0, to = 128)
+    public int getPrefixLength() {
+        return prefixLength;
+    }
+
+    /**
+     * Determines whether the prefix contains the specified address.
+     *
+     * @param address An {@link InetAddress} to test.
+     * @return {@code true} if the prefix covers the given address. {@code false} otherwise.
+     */
+    public boolean contains(@NonNull InetAddress address) {
+        byte[] addrBytes = address.getAddress();
+        if (addrBytes == null || addrBytes.length != this.address.length) {
+            return false;
+        }
+        NetUtils.maskRawAddress(addrBytes, prefixLength);
+        return Arrays.equals(this.address, addrBytes);
+    }
+
+    /**
+     * Returns whether the specified prefix is entirely contained in this prefix.
+     *
+     * Note this is mathematical inclusion, so a prefix is always contained within itself.
+     * @param otherPrefix the prefix to test
+     * @hide
+     */
+    public boolean containsPrefix(@NonNull IpPrefix otherPrefix) {
+        if (otherPrefix.getPrefixLength() < prefixLength) return false;
+        final byte[] otherAddress = otherPrefix.getRawAddress();
+        NetUtils.maskRawAddress(otherAddress, prefixLength);
+        return Arrays.equals(otherAddress, address);
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isIPv6() {
+        return getAddress() instanceof Inet6Address;
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isIPv4() {
+        return getAddress() instanceof Inet4Address;
+    }
+
+    /**
+     * Returns a string representation of this {@code IpPrefix}.
+     *
+     * @return a string such as {@code "192.0.2.0/24"} or {@code "2001:db8:1:2::/64"}.
+     */
+    public String toString() {
+        try {
+            return InetAddress.getByAddress(address).getHostAddress() + "/" + prefixLength;
+        } catch(UnknownHostException e) {
+            // Cosmic rays?
+            throw new IllegalStateException("IpPrefix with invalid address! Shouldn't happen.", e);
+        }
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(address);
+        dest.writeInt(prefixLength);
+    }
+
+    /**
+     * Returns a comparator ordering IpPrefixes by length, shorter to longer.
+     * Contents of the address will break ties.
+     * @hide
+     */
+    public static Comparator<IpPrefix> lengthComparator() {
+        return new Comparator<IpPrefix>() {
+            @Override
+            public int compare(IpPrefix prefix1, IpPrefix prefix2) {
+                if (prefix1.isIPv4()) {
+                    if (prefix2.isIPv6()) return -1;
+                } else {
+                    if (prefix2.isIPv4()) return 1;
+                }
+                final int p1len = prefix1.getPrefixLength();
+                final int p2len = prefix2.getPrefixLength();
+                if (p1len < p2len) return -1;
+                if (p2len < p1len) return 1;
+                final byte[] a1 = prefix1.address;
+                final byte[] a2 = prefix2.address;
+                final int len = a1.length < a2.length ? a1.length : a2.length;
+                for (int i = 0; i < len; ++i) {
+                    if (a1[i] < a2[i]) return -1;
+                    if (a1[i] > a2[i]) return 1;
+                }
+                if (a2.length < len) return 1;
+                if (a1.length < len) return -1;
+                return 0;
+            }
+        };
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public static final @android.annotation.NonNull Creator<IpPrefix> CREATOR =
+            new Creator<IpPrefix>() {
+                public IpPrefix createFromParcel(Parcel in) {
+                    byte[] address = in.createByteArray();
+                    int prefixLength = in.readInt();
+                    return new IpPrefix(address, prefixLength);
+                }
+
+                public IpPrefix[] newArray(int size) {
+                    return new IpPrefix[size];
+                }
+            };
+}
diff --git a/framework/src/android/net/KeepalivePacketData.java b/framework/src/android/net/KeepalivePacketData.java
new file mode 100644
index 0000000..5877f1f
--- /dev/null
+++ b/framework/src/android/net/KeepalivePacketData.java
@@ -0,0 +1,119 @@
+/*
+ * 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.net.InvalidPacketException.ERROR_INVALID_IP_ADDRESS;
+import static android.net.InvalidPacketException.ERROR_INVALID_PORT;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.util.Log;
+
+import com.android.net.module.util.IpUtils;
+
+import java.net.InetAddress;
+
+/**
+ * Represents the actual packets that are sent by the
+ * {@link android.net.SocketKeepalive} API.
+ * @hide
+ */
+@SystemApi
+public class KeepalivePacketData {
+    private static final String TAG = "KeepalivePacketData";
+
+    /** Source IP address */
+    @NonNull
+    private final InetAddress mSrcAddress;
+
+    /** Destination IP address */
+    @NonNull
+    private final InetAddress mDstAddress;
+
+    /** Source port */
+    private final int mSrcPort;
+
+    /** Destination port */
+    private final int mDstPort;
+
+    /** Packet data. A raw byte string of packet data, not including the link-layer header. */
+    private final byte[] mPacket;
+
+    // Note: If you add new fields, please modify the parcelling code in the child classes.
+
+
+    // This should only be constructed via static factory methods, such as
+    // nattKeepalivePacket.
+    /**
+     * A holding class for data necessary to build a keepalive packet.
+     */
+    protected KeepalivePacketData(@NonNull InetAddress srcAddress,
+            @IntRange(from = 0, to = 65535) int srcPort, @NonNull InetAddress dstAddress,
+            @IntRange(from = 0, to = 65535) int dstPort,
+            @NonNull byte[] data) throws InvalidPacketException {
+        this.mSrcAddress = srcAddress;
+        this.mDstAddress = dstAddress;
+        this.mSrcPort = srcPort;
+        this.mDstPort = dstPort;
+        this.mPacket = data;
+
+        // Check we have two IP addresses of the same family.
+        if (srcAddress == null || dstAddress == null || !srcAddress.getClass().getName()
+                .equals(dstAddress.getClass().getName())) {
+            Log.e(TAG, "Invalid or mismatched InetAddresses in KeepalivePacketData");
+            throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+        }
+
+        // Check the ports.
+        if (!IpUtils.isValidUdpOrTcpPort(srcPort) || !IpUtils.isValidUdpOrTcpPort(dstPort)) {
+            Log.e(TAG, "Invalid ports in KeepalivePacketData");
+            throw new InvalidPacketException(ERROR_INVALID_PORT);
+        }
+    }
+
+    /** Get source IP address. */
+    @NonNull
+    public InetAddress getSrcAddress() {
+        return mSrcAddress;
+    }
+
+    /** Get destination IP address. */
+    @NonNull
+    public InetAddress getDstAddress() {
+        return mDstAddress;
+    }
+
+    /** Get source port number. */
+    public int getSrcPort() {
+        return mSrcPort;
+    }
+
+    /** Get destination port number. */
+    public int getDstPort() {
+        return mDstPort;
+    }
+
+    /**
+     * Returns a byte array of the given packet data.
+     */
+    @NonNull
+    public byte[] getPacket() {
+        return mPacket.clone();
+    }
+
+}
diff --git a/framework/src/android/net/LinkAddress.java b/framework/src/android/net/LinkAddress.java
new file mode 100644
index 0000000..d48b8c7
--- /dev/null
+++ b/framework/src/android/net/LinkAddress.java
@@ -0,0 +1,549 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+import static android.system.OsConstants.IFA_F_DADFAILED;
+import static android.system.OsConstants.IFA_F_DEPRECATED;
+import static android.system.OsConstants.IFA_F_OPTIMISTIC;
+import static android.system.OsConstants.IFA_F_PERMANENT;
+import static android.system.OsConstants.IFA_F_TENTATIVE;
+import static android.system.OsConstants.RT_SCOPE_HOST;
+import static android.system.OsConstants.RT_SCOPE_LINK;
+import static android.system.OsConstants.RT_SCOPE_SITE;
+import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
+
+import android.annotation.IntRange;
+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.SystemClock;
+import android.util.Pair;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/**
+ * Identifies an IP address on a network link.
+ *
+ * A {@code LinkAddress} consists of:
+ * <ul>
+ * <li>An IP address and prefix length (e.g., {@code 2001:db8::1/64} or {@code 192.0.2.1/24}).
+ * The address must be unicast, as multicast addresses cannot be assigned to interfaces.
+ * <li>Address flags: A bitmask of {@code OsConstants.IFA_F_*} values representing properties
+ * of the address (e.g., {@code android.system.OsConstants.IFA_F_OPTIMISTIC}).
+ * <li>Address scope: One of the {@code OsConstants.IFA_F_*} values; defines the scope in which
+ * the address is unique (e.g.,
+ * {@code android.system.OsConstants.RT_SCOPE_LINK} or
+ * {@code android.system.OsConstants.RT_SCOPE_UNIVERSE}).
+ * </ul>
+ */
+public class LinkAddress implements Parcelable {
+
+    /**
+     * Indicates the deprecation or expiration time is unknown
+     * @hide
+     */
+    @SystemApi
+    public static final long LIFETIME_UNKNOWN = -1;
+
+    /**
+     * Indicates this address is permanent.
+     * @hide
+     */
+    @SystemApi
+    public static final long LIFETIME_PERMANENT = Long.MAX_VALUE;
+
+    /**
+     * IPv4 or IPv6 address.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private InetAddress address;
+
+    /**
+     * Prefix length.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private int prefixLength;
+
+    /**
+     * Address flags. A bitmask of {@code IFA_F_*} values. Note that {@link #getFlags()} may not
+     * return these exact values. For example, it may set or clear the {@code IFA_F_DEPRECATED}
+     * flag depending on the current preferred lifetime.
+     */
+    private int flags;
+
+    /**
+     * Address scope. One of the RT_SCOPE_* constants.
+     */
+    private int scope;
+
+    /**
+     * The time, as reported by {@link SystemClock#elapsedRealtime}, when this LinkAddress will be
+     * or was deprecated. At the time existing connections can still use this address until it
+     * expires, but new connections should use the new address. {@link #LIFETIME_UNKNOWN} indicates
+     * this information is not available. {@link #LIFETIME_PERMANENT} indicates this
+     * {@link LinkAddress} will never be deprecated.
+     */
+    private long deprecationTime;
+
+    /**
+     * The time, as reported by {@link SystemClock#elapsedRealtime}, when this {@link LinkAddress}
+     * will expire and be removed from the interface. {@link #LIFETIME_UNKNOWN} indicates this
+     * information is not available. {@link #LIFETIME_PERMANENT} indicates this {@link LinkAddress}
+     * will never expire.
+     */
+    private long expirationTime;
+
+    /**
+     * Utility function to determines the scope of a unicast address. Per RFC 4291 section 2.5 and
+     * RFC 6724 section 3.2.
+     * @hide
+     */
+    private static int scopeForUnicastAddress(InetAddress addr) {
+        if (addr.isAnyLocalAddress()) {
+            return RT_SCOPE_HOST;
+        }
+
+        if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
+            return RT_SCOPE_LINK;
+        }
+
+        // isSiteLocalAddress() returns true for private IPv4 addresses, but RFC 6724 section 3.2
+        // says that they are assigned global scope.
+        if (!(addr instanceof Inet4Address) && addr.isSiteLocalAddress()) {
+            return RT_SCOPE_SITE;
+        }
+
+        return RT_SCOPE_UNIVERSE;
+    }
+
+    /**
+     * Utility function to check if |address| is a Unique Local IPv6 Unicast Address
+     * (a.k.a. "ULA"; RFC 4193).
+     *
+     * Per RFC 4193 section 8, fc00::/7 identifies these addresses.
+     */
+    private boolean isIpv6ULA() {
+        if (isIpv6()) {
+            byte[] bytes = address.getAddress();
+            return ((bytes[0] & (byte)0xfe) == (byte)0xfc);
+        }
+        return false;
+    }
+
+    /**
+     * @return true if the address is IPv6.
+     * @hide
+     */
+    @SystemApi
+    public boolean isIpv6() {
+        return address instanceof Inet6Address;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return true if the address is IPv6.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean isIPv6() {
+        return isIpv6();
+    }
+
+    /**
+     * @return true if the address is IPv4 or is a mapped IPv4 address.
+     * @hide
+     */
+    @SystemApi
+    public boolean isIpv4() {
+        return address instanceof Inet4Address;
+    }
+
+    /**
+     * Utility function for the constructors.
+     */
+    private void init(InetAddress address, int prefixLength, int flags, int scope,
+                      long deprecationTime, long expirationTime) {
+        if (address == null ||
+                address.isMulticastAddress() ||
+                prefixLength < 0 ||
+                (address instanceof Inet4Address && prefixLength > 32) ||
+                (prefixLength > 128)) {
+            throw new IllegalArgumentException("Bad LinkAddress params " + address +
+                    "/" + prefixLength);
+        }
+
+        // deprecation time and expiration time must be both provided, or neither.
+        if ((deprecationTime == LIFETIME_UNKNOWN) != (expirationTime == LIFETIME_UNKNOWN)) {
+            throw new IllegalArgumentException(
+                    "Must not specify only one of deprecation time and expiration time");
+        }
+
+        // deprecation time needs to be a positive value.
+        if (deprecationTime != LIFETIME_UNKNOWN && deprecationTime < 0) {
+            throw new IllegalArgumentException("invalid deprecation time " + deprecationTime);
+        }
+
+        // expiration time needs to be a positive value.
+        if (expirationTime != LIFETIME_UNKNOWN && expirationTime < 0) {
+            throw new IllegalArgumentException("invalid expiration time " + expirationTime);
+        }
+
+        // expiration time can't be earlier than deprecation time
+        if (deprecationTime != LIFETIME_UNKNOWN && expirationTime != LIFETIME_UNKNOWN
+                && expirationTime < deprecationTime) {
+            throw new IllegalArgumentException("expiration earlier than deprecation ("
+                    + deprecationTime + ", " + expirationTime + ")");
+        }
+
+        this.address = address;
+        this.prefixLength = prefixLength;
+        this.flags = flags;
+        this.scope = scope;
+        this.deprecationTime = deprecationTime;
+        this.expirationTime = expirationTime;
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from an {@code InetAddress} and prefix length, with
+     * the specified flags and scope. Flags and scope are not checked for validity.
+     *
+     * @param address The IP address.
+     * @param prefixLength The prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
+     * @param flags A bitmask of {@code IFA_F_*} values representing properties of the address.
+     * @param scope An integer defining the scope in which the address is unique (e.g.,
+     *              {@link OsConstants#RT_SCOPE_LINK} or {@link OsConstants#RT_SCOPE_SITE}).
+     * @hide
+     */
+    @SystemApi
+    public LinkAddress(@NonNull InetAddress address, @IntRange(from = 0, to = 128) int prefixLength,
+            int flags, int scope) {
+        init(address, prefixLength, flags, scope, LIFETIME_UNKNOWN, LIFETIME_UNKNOWN);
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from an {@code InetAddress}, prefix length, with
+     * the specified flags, scope, deprecation time, and expiration time. Flags and scope are not
+     * checked for validity. The value of the {@code IFA_F_DEPRECATED} and {@code IFA_F_PERMANENT}
+     * flag will be adjusted based on the passed-in lifetimes.
+     *
+     * @param address The IP address.
+     * @param prefixLength The prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
+     * @param flags A bitmask of {@code IFA_F_*} values representing properties of the address.
+     * @param scope An integer defining the scope in which the address is unique (e.g.,
+     *              {@link OsConstants#RT_SCOPE_LINK} or {@link OsConstants#RT_SCOPE_SITE}).
+     * @param deprecationTime The time, as reported by {@link SystemClock#elapsedRealtime}, when
+     *                        this {@link LinkAddress} will be or was deprecated. At the time
+     *                        existing connections can still use this address until it expires, but
+     *                        new connections should use the new address. {@link #LIFETIME_UNKNOWN}
+     *                        indicates this information is not available.
+     *                        {@link #LIFETIME_PERMANENT} indicates this {@link LinkAddress} will
+     *                        never be deprecated.
+     * @param expirationTime The time, as reported by {@link SystemClock#elapsedRealtime}, when this
+     *                       {@link LinkAddress} will expire and be removed from the interface.
+     *                       {@link #LIFETIME_UNKNOWN} indicates this information is not available.
+     *                       {@link #LIFETIME_PERMANENT} indicates this {@link LinkAddress} will
+     *                       never expire.
+     * @hide
+     */
+    @SystemApi
+    public LinkAddress(@NonNull InetAddress address, @IntRange(from = 0, to = 128) int prefixLength,
+                       int flags, int scope, long deprecationTime, long expirationTime) {
+        init(address, prefixLength, flags, scope, deprecationTime, expirationTime);
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from an {@code InetAddress} and a prefix length.
+     * The flags are set to zero and the scope is determined from the address.
+     * @param address The IP address.
+     * @param prefixLength The prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
+     * @hide
+     */
+    @SystemApi
+    public LinkAddress(@NonNull InetAddress address,
+            @IntRange(from = 0, to = 128) int prefixLength) {
+        this(address, prefixLength, 0, 0);
+        this.scope = scopeForUnicastAddress(address);
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from an {@code InterfaceAddress}.
+     * The flags are set to zero and the scope is determined from the address.
+     * @param interfaceAddress The interface address.
+     * @hide
+     */
+    public LinkAddress(@NonNull InterfaceAddress interfaceAddress) {
+        this(interfaceAddress.getAddress(),
+             interfaceAddress.getNetworkPrefixLength());
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from a string such as "192.0.2.5/24" or
+     * "2001:db8::1/64". The flags are set to zero and the scope is determined from the address.
+     * @param address The string to parse.
+     * @hide
+     */
+    @SystemApi
+    public LinkAddress(@NonNull String address) {
+        this(address, 0, 0);
+        this.scope = scopeForUnicastAddress(this.address);
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from a string such as "192.0.2.5/24" or
+     * "2001:db8::1/64", with the specified flags and scope.
+     * @param address The string to parse.
+     * @param flags The address flags.
+     * @param scope The address scope.
+     * @hide
+     */
+    @SystemApi
+    public LinkAddress(@NonNull String address, int flags, int scope) {
+        // This may throw an IllegalArgumentException; catching it is the caller's responsibility.
+        // TODO: consider rejecting mapped IPv4 addresses such as "::ffff:192.0.2.5/24".
+        Pair<InetAddress, Integer> ipAndMask = NetworkUtils.legacyParseIpAndMask(address);
+        init(ipAndMask.first, ipAndMask.second, flags, scope, LIFETIME_UNKNOWN, LIFETIME_UNKNOWN);
+    }
+
+    /**
+     * Returns a string representation of this address, such as "192.0.2.1/24" or "2001:db8::1/64".
+     * The string representation does not contain the flags and scope, just the address and prefix
+     * length.
+     */
+    @Override
+    public String toString() {
+        return address.getHostAddress() + "/" + prefixLength;
+    }
+
+    /**
+     * Compares this {@code LinkAddress} instance against {@code obj}. Two addresses are equal if
+     * their address, prefix length, flags and scope are equal. Thus, for example, two addresses
+     * that have the same address and prefix length are not equal if one of them is deprecated and
+     * the other is not.
+     *
+     * @param obj the object to be tested for equality.
+     * @return {@code true} if both objects are equal, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof LinkAddress)) {
+            return false;
+        }
+        LinkAddress linkAddress = (LinkAddress) obj;
+        return this.address.equals(linkAddress.address)
+                && this.prefixLength == linkAddress.prefixLength
+                && this.flags == linkAddress.flags
+                && this.scope == linkAddress.scope
+                && this.deprecationTime == linkAddress.deprecationTime
+                && this.expirationTime == linkAddress.expirationTime;
+    }
+
+    /**
+     * Returns a hashcode for this address.
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(address, prefixLength, flags, scope, deprecationTime, expirationTime);
+    }
+
+    /**
+     * Determines whether this {@code LinkAddress} and the provided {@code LinkAddress}
+     * represent the same address. Two {@code LinkAddresses} represent the same address
+     * if they have the same IP address and prefix length, even if their properties are
+     * different.
+     *
+     * @param other the {@code LinkAddress} to compare to.
+     * @return {@code true} if both objects have the same address and prefix length, {@code false}
+     * otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean isSameAddressAs(@Nullable LinkAddress other) {
+        if (other == null) {
+            return false;
+        }
+        return address.equals(other.address) && prefixLength == other.prefixLength;
+    }
+
+    /**
+     * Returns the {@link InetAddress} of this {@code LinkAddress}.
+     */
+    public InetAddress getAddress() {
+        return address;
+    }
+
+    /**
+     * Returns the prefix length of this {@code LinkAddress}.
+     */
+    @IntRange(from = 0, to = 128)
+    public int getPrefixLength() {
+        return prefixLength;
+    }
+
+    /**
+     * Returns the prefix length of this {@code LinkAddress}.
+     * TODO: Delete all callers and remove in favour of getPrefixLength().
+     * @hide
+     */
+    @UnsupportedAppUsage
+    @IntRange(from = 0, to = 128)
+    public int getNetworkPrefixLength() {
+        return getPrefixLength();
+    }
+
+    /**
+     * Returns the flags of this {@code LinkAddress}.
+     */
+    public int getFlags() {
+        int flags = this.flags;
+        if (deprecationTime != LIFETIME_UNKNOWN) {
+            if (SystemClock.elapsedRealtime() >= deprecationTime) {
+                flags |= IFA_F_DEPRECATED;
+            } else {
+                // If deprecation time is in the future, or permanent.
+                flags &= ~IFA_F_DEPRECATED;
+            }
+        }
+
+        if (expirationTime == LIFETIME_PERMANENT) {
+            flags |= IFA_F_PERMANENT;
+        } else if (expirationTime != LIFETIME_UNKNOWN) {
+            // If we know this address expired or will expire in the future, then this address
+            // should not be permanent.
+            flags &= ~IFA_F_PERMANENT;
+        }
+
+        // Do no touch the original flags. Return the adjusted flags here.
+        return flags;
+    }
+
+    /**
+     * Returns the scope of this {@code LinkAddress}.
+     */
+    public int getScope() {
+        return scope;
+    }
+
+    /**
+     * Get the deprecation time, as reported by {@link SystemClock#elapsedRealtime}, when this
+     * {@link LinkAddress} will be or was deprecated. At the time existing connections can still use
+     * this address until it expires, but new connections should use the new address.
+     *
+     * @return The deprecation time in milliseconds. {@link #LIFETIME_UNKNOWN} indicates this
+     * information is not available. {@link #LIFETIME_PERMANENT} indicates this {@link LinkAddress}
+     * will never be deprecated.
+     *
+     * @hide
+     */
+    @SystemApi
+    public long getDeprecationTime() {
+        return deprecationTime;
+    }
+
+    /**
+     * Get the expiration time, as reported by {@link SystemClock#elapsedRealtime}, when this
+     * {@link LinkAddress} will expire and be removed from the interface.
+     *
+     * @return The expiration time in milliseconds. {@link #LIFETIME_UNKNOWN} indicates this
+     * information is not available. {@link #LIFETIME_PERMANENT} indicates this {@link LinkAddress}
+     * will never expire.
+     *
+     * @hide
+     */
+    @SystemApi
+    public long getExpirationTime() {
+        return expirationTime;
+    }
+
+    /**
+     * Returns true if this {@code LinkAddress} is global scope and preferred (i.e., not currently
+     * deprecated).
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean isGlobalPreferred() {
+        /**
+         * Note that addresses flagged as IFA_F_OPTIMISTIC are
+         * simultaneously flagged as IFA_F_TENTATIVE (when the tentative
+         * state has cleared either DAD has succeeded or failed, and both
+         * flags are cleared regardless).
+         */
+        int flags = getFlags();
+        return (scope == RT_SCOPE_UNIVERSE
+                && !isIpv6ULA()
+                && (flags & (IFA_F_DADFAILED | IFA_F_DEPRECATED)) == 0L
+                && ((flags & IFA_F_TENTATIVE) == 0L || (flags & IFA_F_OPTIMISTIC) != 0L));
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(address.getAddress());
+        dest.writeInt(prefixLength);
+        dest.writeInt(this.flags);
+        dest.writeInt(scope);
+        dest.writeLong(deprecationTime);
+        dest.writeLong(expirationTime);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public static final @android.annotation.NonNull Creator<LinkAddress> CREATOR =
+        new Creator<LinkAddress>() {
+            public LinkAddress createFromParcel(Parcel in) {
+                InetAddress address = null;
+                try {
+                    address = InetAddress.getByAddress(in.createByteArray());
+                } catch (UnknownHostException e) {
+                    // Nothing we can do here. When we call the constructor, we'll throw an
+                    // IllegalArgumentException, because a LinkAddress can't have a null
+                    // InetAddress.
+                }
+                int prefixLength = in.readInt();
+                int flags = in.readInt();
+                int scope = in.readInt();
+                long deprecationTime = in.readLong();
+                long expirationTime = in.readLong();
+                return new LinkAddress(address, prefixLength, flags, scope, deprecationTime,
+                        expirationTime);
+            }
+
+            public LinkAddress[] newArray(int size) {
+                return new LinkAddress[size];
+            }
+        };
+}
diff --git a/framework/src/android/net/LinkProperties.java b/framework/src/android/net/LinkProperties.java
new file mode 100644
index 0000000..99f48b4
--- /dev/null
+++ b/framework/src/android/net/LinkProperties.java
@@ -0,0 +1,1823 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+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.text.TextUtils;
+
+import com.android.net.module.util.LinkPropertiesUtils;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Objects;
+import java.util.StringJoiner;
+
+/**
+ * Describes the properties of a network link.
+ *
+ * A link represents a connection to a network.
+ * It may have multiple addresses and multiple gateways,
+ * multiple dns servers but only one http proxy and one
+ * network interface.
+ *
+ * Note that this is just a holder of data.  Modifying it
+ * does not affect live networks.
+ *
+ */
+public final class LinkProperties implements Parcelable {
+    // The interface described by the network link.
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private String mIfaceName;
+    private final ArrayList<LinkAddress> mLinkAddresses = new ArrayList<>();
+    private final ArrayList<InetAddress> mDnses = new ArrayList<>();
+    // PCSCF addresses are addresses of SIP proxies that only exist for the IMS core service.
+    private final ArrayList<InetAddress> mPcscfs = new ArrayList<InetAddress>();
+    private final ArrayList<InetAddress> mValidatedPrivateDnses = new ArrayList<>();
+    private boolean mUsePrivateDns;
+    private String mPrivateDnsServerName;
+    private String mDomains;
+    private ArrayList<RouteInfo> mRoutes = new ArrayList<>();
+    private Inet4Address mDhcpServerAddress;
+    private ProxyInfo mHttpProxy;
+    private int mMtu;
+    // in the format "rmem_min,rmem_def,rmem_max,wmem_min,wmem_def,wmem_max"
+    private String mTcpBufferSizes;
+    private IpPrefix mNat64Prefix;
+    private boolean mWakeOnLanSupported;
+    private Uri mCaptivePortalApiUrl;
+    private CaptivePortalData mCaptivePortalData;
+
+    /**
+     * Indicates whether parceling should preserve fields that are set based on permissions of
+     * the process receiving the {@link LinkProperties}.
+     */
+    private final transient boolean mParcelSensitiveFields;
+
+    private static final int MIN_MTU    = 68;
+
+    private static final int MIN_MTU_V6 = 1280;
+
+    private static final int MAX_MTU    = 10000;
+
+    private static final int INET6_ADDR_LENGTH = 16;
+
+    // Stores the properties of links that are "stacked" above this link.
+    // Indexed by interface name to allow modification and to prevent duplicates being added.
+    private Hashtable<String, LinkProperties> mStackedLinks = new Hashtable<>();
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage(implicitMember =
+            "values()[Landroid/net/LinkProperties$ProvisioningChange;")
+    public enum ProvisioningChange {
+        @UnsupportedAppUsage
+        STILL_NOT_PROVISIONED,
+        @UnsupportedAppUsage
+        LOST_PROVISIONING,
+        @UnsupportedAppUsage
+        GAINED_PROVISIONING,
+        @UnsupportedAppUsage
+        STILL_PROVISIONED,
+    }
+
+    /**
+     * Compare the provisioning states of two LinkProperties instances.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static ProvisioningChange compareProvisioning(
+            LinkProperties before, LinkProperties after) {
+        if (before.isProvisioned() && after.isProvisioned()) {
+            // On dual-stack networks, DHCPv4 renewals can occasionally fail.
+            // When this happens, IPv6-reachable services continue to function
+            // normally but IPv4-only services (naturally) fail.
+            //
+            // When an application using an IPv4-only service reports a bad
+            // network condition to the framework, attempts to re-validate
+            // the network succeed (since we support IPv6-only networks) and
+            // nothing is changed.
+            //
+            // For users, this is confusing and unexpected behaviour, and is
+            // not necessarily easy to diagnose.  Therefore, we treat changing
+            // from a dual-stack network to an IPv6-only network equivalent to
+            // a total loss of provisioning.
+            //
+            // For one such example of this, see b/18867306.
+            //
+            // Additionally, losing IPv6 provisioning can result in TCP
+            // connections getting stuck until timeouts fire and other
+            // baffling failures. Therefore, loss of either IPv4 or IPv6 on a
+            // previously dual-stack network is deemed a lost of provisioning.
+            if ((before.isIpv4Provisioned() && !after.isIpv4Provisioned())
+                    || (before.isIpv6Provisioned() && !after.isIpv6Provisioned())) {
+                return ProvisioningChange.LOST_PROVISIONING;
+            }
+            return ProvisioningChange.STILL_PROVISIONED;
+        } else if (before.isProvisioned() && !after.isProvisioned()) {
+            return ProvisioningChange.LOST_PROVISIONING;
+        } else if (!before.isProvisioned() && after.isProvisioned()) {
+            return ProvisioningChange.GAINED_PROVISIONING;
+        } else {  // !before.isProvisioned() && !after.isProvisioned()
+            return ProvisioningChange.STILL_NOT_PROVISIONED;
+        }
+    }
+
+    /**
+     * Constructs a new {@code LinkProperties} with default values.
+     */
+    public LinkProperties() {
+        mParcelSensitiveFields = false;
+    }
+
+    /**
+     * @hide
+     */
+    @SystemApi
+    public LinkProperties(@Nullable LinkProperties source) {
+        this(source, false /* parcelSensitiveFields */);
+    }
+
+    /**
+     * Create a copy of a {@link LinkProperties} that may preserve fields that were set
+     * based on the permissions of the process that originally received it.
+     *
+     * <p>By default {@link LinkProperties} does not preserve such fields during parceling, as
+     * they should not be shared outside of the process that receives them without appropriate
+     * checks.
+     * @param parcelSensitiveFields Whether the sensitive fields should be kept when parceling
+     * @hide
+     */
+    @SystemApi
+    public LinkProperties(@Nullable LinkProperties source, boolean parcelSensitiveFields) {
+        mParcelSensitiveFields = parcelSensitiveFields;
+        if (source == null) return;
+        mIfaceName = source.mIfaceName;
+        mLinkAddresses.addAll(source.mLinkAddresses);
+        mDnses.addAll(source.mDnses);
+        mValidatedPrivateDnses.addAll(source.mValidatedPrivateDnses);
+        mUsePrivateDns = source.mUsePrivateDns;
+        mPrivateDnsServerName = source.mPrivateDnsServerName;
+        mPcscfs.addAll(source.mPcscfs);
+        mDomains = source.mDomains;
+        mRoutes.addAll(source.mRoutes);
+        mHttpProxy = (source.mHttpProxy == null) ? null : new ProxyInfo(source.mHttpProxy);
+        for (LinkProperties l: source.mStackedLinks.values()) {
+            addStackedLink(l);
+        }
+        setMtu(source.mMtu);
+        setDhcpServerAddress(source.getDhcpServerAddress());
+        mTcpBufferSizes = source.mTcpBufferSizes;
+        mNat64Prefix = source.mNat64Prefix;
+        mWakeOnLanSupported = source.mWakeOnLanSupported;
+        mCaptivePortalApiUrl = source.mCaptivePortalApiUrl;
+        mCaptivePortalData = source.mCaptivePortalData;
+    }
+
+    /**
+     * Sets the interface name for this link.  All {@link RouteInfo} already set for this
+     * will have their interface changed to match this new value.
+     *
+     * @param iface The name of the network interface used for this link.
+     */
+    public void setInterfaceName(@Nullable String iface) {
+        mIfaceName = iface;
+        ArrayList<RouteInfo> newRoutes = new ArrayList<>(mRoutes.size());
+        for (RouteInfo route : mRoutes) {
+            newRoutes.add(routeWithInterface(route));
+        }
+        mRoutes = newRoutes;
+    }
+
+    /**
+     * Gets the interface name for this link.  May be {@code null} if not set.
+     *
+     * @return The interface name set for this link or {@code null}.
+     */
+    public @Nullable String getInterfaceName() {
+        return mIfaceName;
+    }
+
+    /**
+     * @hide
+     */
+    @SystemApi
+    public @NonNull List<String> getAllInterfaceNames() {
+        List<String> interfaceNames = new ArrayList<>(mStackedLinks.size() + 1);
+        if (mIfaceName != null) interfaceNames.add(mIfaceName);
+        for (LinkProperties stacked: mStackedLinks.values()) {
+            interfaceNames.addAll(stacked.getAllInterfaceNames());
+        }
+        return interfaceNames;
+    }
+
+    /**
+     * Returns all the addresses on this link.  We often think of a link having a single address,
+     * however, particularly with Ipv6 several addresses are typical.  Note that the
+     * {@code LinkProperties} actually contains {@link LinkAddress} objects which also include
+     * prefix lengths for each address.  This is a simplified utility alternative to
+     * {@link LinkProperties#getLinkAddresses}.
+     *
+     * @return An unmodifiable {@link List} of {@link InetAddress} for this link.
+     * @hide
+     */
+    @SystemApi
+    public @NonNull List<InetAddress> getAddresses() {
+        final List<InetAddress> addresses = new ArrayList<>();
+        for (LinkAddress linkAddress : mLinkAddresses) {
+            addresses.add(linkAddress.getAddress());
+        }
+        return Collections.unmodifiableList(addresses);
+    }
+
+    /**
+     * Returns all the addresses on this link and all the links stacked above it.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public @NonNull List<InetAddress> getAllAddresses() {
+        List<InetAddress> addresses = new ArrayList<>();
+        for (LinkAddress linkAddress : mLinkAddresses) {
+            addresses.add(linkAddress.getAddress());
+        }
+        for (LinkProperties stacked: mStackedLinks.values()) {
+            addresses.addAll(stacked.getAllAddresses());
+        }
+        return addresses;
+    }
+
+    private int findLinkAddressIndex(LinkAddress address) {
+        for (int i = 0; i < mLinkAddresses.size(); i++) {
+            if (mLinkAddresses.get(i).isSameAddressAs(address)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Adds a {@link LinkAddress} to this {@code LinkProperties} if a {@link LinkAddress} of the
+     * same address/prefix does not already exist.  If it does exist it is replaced.
+     * @param address The {@code LinkAddress} to add.
+     * @return true if {@code address} was added or updated, false otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean addLinkAddress(@NonNull LinkAddress address) {
+        if (address == null) {
+            return false;
+        }
+        int i = findLinkAddressIndex(address);
+        if (i < 0) {
+            // Address was not present. Add it.
+            mLinkAddresses.add(address);
+            return true;
+        } else if (mLinkAddresses.get(i).equals(address)) {
+            // Address was present and has same properties. Do nothing.
+            return false;
+        } else {
+            // Address was present and has different properties. Update it.
+            mLinkAddresses.set(i, address);
+            return true;
+        }
+    }
+
+    /**
+     * Removes a {@link LinkAddress} from this {@code LinkProperties}.  Specifically, matches
+     * and {@link LinkAddress} with the same address and prefix.
+     *
+     * @param toRemove A {@link LinkAddress} specifying the address to remove.
+     * @return true if the address was removed, false if it did not exist.
+     * @hide
+     */
+    @SystemApi
+    public boolean removeLinkAddress(@NonNull LinkAddress toRemove) {
+        int i = findLinkAddressIndex(toRemove);
+        if (i >= 0) {
+            mLinkAddresses.remove(i);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns all the {@link LinkAddress} on this link.  Typically a link will have
+     * one IPv4 address and one or more IPv6 addresses.
+     *
+     * @return An unmodifiable {@link List} of {@link LinkAddress} for this link.
+     */
+    public @NonNull List<LinkAddress> getLinkAddresses() {
+        return Collections.unmodifiableList(mLinkAddresses);
+    }
+
+    /**
+     * Returns all the addresses on this link and all the links stacked above it.
+     * @hide
+     */
+    @SystemApi
+    public @NonNull List<LinkAddress> getAllLinkAddresses() {
+        List<LinkAddress> addresses = new ArrayList<>(mLinkAddresses);
+        for (LinkProperties stacked: mStackedLinks.values()) {
+            addresses.addAll(stacked.getAllLinkAddresses());
+        }
+        return addresses;
+    }
+
+    /**
+     * Replaces the {@link LinkAddress} in this {@code LinkProperties} with
+     * the given {@link Collection} of {@link LinkAddress}.
+     *
+     * @param addresses The {@link Collection} of {@link LinkAddress} to set in this
+     *                  object.
+     */
+    public void setLinkAddresses(@NonNull Collection<LinkAddress> addresses) {
+        mLinkAddresses.clear();
+        for (LinkAddress address: addresses) {
+            addLinkAddress(address);
+        }
+    }
+
+    /**
+     * Adds the given {@link InetAddress} to the list of DNS servers, if not present.
+     *
+     * @param dnsServer The {@link InetAddress} to add to the list of DNS servers.
+     * @return true if the DNS server was added, false if it was already present.
+     * @hide
+     */
+    @SystemApi
+    public boolean addDnsServer(@NonNull InetAddress dnsServer) {
+        if (dnsServer != null && !mDnses.contains(dnsServer)) {
+            mDnses.add(dnsServer);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the given {@link InetAddress} from the list of DNS servers.
+     *
+     * @param dnsServer The {@link InetAddress} to remove from the list of DNS servers.
+     * @return true if the DNS server was removed, false if it did not exist.
+     * @hide
+     */
+    @SystemApi
+    public boolean removeDnsServer(@NonNull InetAddress dnsServer) {
+        return mDnses.remove(dnsServer);
+    }
+
+    /**
+     * Replaces the DNS servers in this {@code LinkProperties} with
+     * the given {@link Collection} of {@link InetAddress} objects.
+     *
+     * @param dnsServers The {@link Collection} of DNS servers to set in this object.
+     */
+    public void setDnsServers(@NonNull Collection<InetAddress> dnsServers) {
+        mDnses.clear();
+        for (InetAddress dnsServer: dnsServers) {
+            addDnsServer(dnsServer);
+        }
+    }
+
+    /**
+     * Returns all the {@link InetAddress} for DNS servers on this link.
+     *
+     * @return An unmodifiable {@link List} of {@link InetAddress} for DNS servers on
+     *         this link.
+     */
+    public @NonNull List<InetAddress> getDnsServers() {
+        return Collections.unmodifiableList(mDnses);
+    }
+
+    /**
+     * Set whether private DNS is currently in use on this network.
+     *
+     * @param usePrivateDns The private DNS state.
+     * @hide
+     */
+    @SystemApi
+    public void setUsePrivateDns(boolean usePrivateDns) {
+        mUsePrivateDns = usePrivateDns;
+    }
+
+    /**
+     * Returns whether private DNS is currently in use on this network. When
+     * private DNS is in use, applications must not send unencrypted DNS
+     * queries as doing so could reveal private user information. Furthermore,
+     * if private DNS is in use and {@link #getPrivateDnsServerName} is not
+     * {@code null}, DNS queries must be sent to the specified DNS server.
+     *
+     * @return {@code true} if private DNS is in use, {@code false} otherwise.
+     */
+    public boolean isPrivateDnsActive() {
+        return mUsePrivateDns;
+    }
+
+    /**
+     * Set the name of the private DNS server to which private DNS queries
+     * should be sent when in strict mode. This value should be {@code null}
+     * when private DNS is off or in opportunistic mode.
+     *
+     * @param privateDnsServerName The private DNS server name.
+     * @hide
+     */
+    @SystemApi
+    public void setPrivateDnsServerName(@Nullable String privateDnsServerName) {
+        mPrivateDnsServerName = privateDnsServerName;
+    }
+
+    /**
+     * Set DHCP server address.
+     *
+     * @param serverAddress the server address to set.
+     */
+    public void setDhcpServerAddress(@Nullable Inet4Address serverAddress) {
+        mDhcpServerAddress = serverAddress;
+    }
+
+     /**
+     * Get DHCP server address
+     *
+     * @return The current DHCP server address.
+     */
+    public @Nullable Inet4Address getDhcpServerAddress() {
+        return mDhcpServerAddress;
+    }
+
+    /**
+     * Returns the private DNS server name that is in use. If not {@code null},
+     * private DNS is in strict mode. In this mode, applications should ensure
+     * that all DNS queries are encrypted and sent to this hostname and that
+     * queries are only sent if the hostname's certificate is valid. If
+     * {@code null} and {@link #isPrivateDnsActive} is {@code true}, private
+     * DNS is in opportunistic mode, and applications should ensure that DNS
+     * queries are encrypted and sent to a DNS server returned by
+     * {@link #getDnsServers}. System DNS will handle each of these cases
+     * correctly, but applications implementing their own DNS lookups must make
+     * sure to follow these requirements.
+     *
+     * @return The private DNS server name.
+     */
+    public @Nullable String getPrivateDnsServerName() {
+        return mPrivateDnsServerName;
+    }
+
+    /**
+     * Adds the given {@link InetAddress} to the list of validated private DNS servers,
+     * if not present. This is distinct from the server name in that these are actually
+     * resolved addresses.
+     *
+     * @param dnsServer The {@link InetAddress} to add to the list of validated private DNS servers.
+     * @return true if the DNS server was added, false if it was already present.
+     * @hide
+     */
+    public boolean addValidatedPrivateDnsServer(@NonNull InetAddress dnsServer) {
+        if (dnsServer != null && !mValidatedPrivateDnses.contains(dnsServer)) {
+            mValidatedPrivateDnses.add(dnsServer);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the given {@link InetAddress} from the list of validated private DNS servers.
+     *
+     * @param dnsServer The {@link InetAddress} to remove from the list of validated private DNS
+     *        servers.
+     * @return true if the DNS server was removed, false if it did not exist.
+     * @hide
+     */
+    public boolean removeValidatedPrivateDnsServer(@NonNull InetAddress dnsServer) {
+        return mValidatedPrivateDnses.remove(dnsServer);
+    }
+
+    /**
+     * Replaces the validated private DNS servers in this {@code LinkProperties} with
+     * the given {@link Collection} of {@link InetAddress} objects.
+     *
+     * @param dnsServers The {@link Collection} of validated private DNS servers to set in this
+     *        object.
+     * @hide
+     */
+    @SystemApi
+    public void setValidatedPrivateDnsServers(@NonNull Collection<InetAddress> dnsServers) {
+        mValidatedPrivateDnses.clear();
+        for (InetAddress dnsServer: dnsServers) {
+            addValidatedPrivateDnsServer(dnsServer);
+        }
+    }
+
+    /**
+     * Returns all the {@link InetAddress} for validated private DNS servers on this link.
+     * These are resolved from the private DNS server name.
+     *
+     * @return An unmodifiable {@link List} of {@link InetAddress} for validated private
+     *         DNS servers on this link.
+     * @hide
+     */
+    @SystemApi
+    public @NonNull List<InetAddress> getValidatedPrivateDnsServers() {
+        return Collections.unmodifiableList(mValidatedPrivateDnses);
+    }
+
+    /**
+     * Adds the given {@link InetAddress} to the list of PCSCF servers, if not present.
+     *
+     * @param pcscfServer The {@link InetAddress} to add to the list of PCSCF servers.
+     * @return true if the PCSCF server was added, false otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean addPcscfServer(@NonNull InetAddress pcscfServer) {
+        if (pcscfServer != null && !mPcscfs.contains(pcscfServer)) {
+            mPcscfs.add(pcscfServer);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the given {@link InetAddress} from the list of PCSCF servers.
+     *
+     * @param pcscfServer The {@link InetAddress} to remove from the list of PCSCF servers.
+     * @return true if the PCSCF server was removed, false otherwise.
+     * @hide
+     */
+    public boolean removePcscfServer(@NonNull InetAddress pcscfServer) {
+        return mPcscfs.remove(pcscfServer);
+    }
+
+    /**
+     * Replaces the PCSCF servers in this {@code LinkProperties} with
+     * the given {@link Collection} of {@link InetAddress} objects.
+     *
+     * @param pcscfServers The {@link Collection} of PCSCF servers to set in this object.
+     * @hide
+     */
+    @SystemApi
+    public void setPcscfServers(@NonNull Collection<InetAddress> pcscfServers) {
+        mPcscfs.clear();
+        for (InetAddress pcscfServer: pcscfServers) {
+            addPcscfServer(pcscfServer);
+        }
+    }
+
+    /**
+     * Returns all the {@link InetAddress} for PCSCF servers on this link.
+     *
+     * @return An unmodifiable {@link List} of {@link InetAddress} for PCSCF servers on
+     *         this link.
+     * @hide
+     */
+    @SystemApi
+    public @NonNull List<InetAddress> getPcscfServers() {
+        return Collections.unmodifiableList(mPcscfs);
+    }
+
+    /**
+     * Sets the DNS domain search path used on this link.
+     *
+     * @param domains A {@link String} listing in priority order the comma separated
+     *                domains to search when resolving host names on this link.
+     */
+    public void setDomains(@Nullable String domains) {
+        mDomains = domains;
+    }
+
+    /**
+     * Get the DNS domains search path set for this link. May be {@code null} if not set.
+     *
+     * @return A {@link String} containing the comma separated domains to search when resolving host
+     *         names on this link or {@code null}.
+     */
+    public @Nullable String getDomains() {
+        return mDomains;
+    }
+
+    /**
+     * Sets the Maximum Transmission Unit size to use on this link.  This should not be used
+     * unless the system default (1500) is incorrect.  Values less than 68 or greater than
+     * 10000 will be ignored.
+     *
+     * @param mtu The MTU to use for this link.
+     */
+    public void setMtu(int mtu) {
+        mMtu = mtu;
+    }
+
+    /**
+     * Gets any non-default MTU size set for this link.  Note that if the default is being used
+     * this will return 0.
+     *
+     * @return The mtu value set for this link.
+     */
+    public int getMtu() {
+        return mMtu;
+    }
+
+    /**
+     * Sets the tcp buffers sizes to be used when this link is the system default.
+     * Should be of the form "rmem_min,rmem_def,rmem_max,wmem_min,wmem_def,wmem_max".
+     *
+     * @param tcpBufferSizes The tcp buffers sizes to use.
+     *
+     * @hide
+     */
+    @SystemApi
+    public void setTcpBufferSizes(@Nullable String tcpBufferSizes) {
+        mTcpBufferSizes = tcpBufferSizes;
+    }
+
+    /**
+     * Gets the tcp buffer sizes. May be {@code null} if not set.
+     *
+     * @return the tcp buffer sizes to use when this link is the system default or {@code null}.
+     *
+     * @hide
+     */
+    @SystemApi
+    public @Nullable String getTcpBufferSizes() {
+        return mTcpBufferSizes;
+    }
+
+    private RouteInfo routeWithInterface(RouteInfo route) {
+        return new RouteInfo(
+            route.getDestination(),
+            route.getGateway(),
+            mIfaceName,
+            route.getType(),
+            route.getMtu());
+    }
+
+    private int findRouteIndexByRouteKey(RouteInfo route) {
+        for (int i = 0; i < mRoutes.size(); i++) {
+            if (mRoutes.get(i).getRouteKey().equals(route.getRouteKey())) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Adds a {@link RouteInfo} to this {@code LinkProperties}. If there is a {@link RouteInfo}
+     * with the same destination, gateway and interface with different properties
+     * (e.g., different MTU), it will be updated. If the {@link RouteInfo} had an
+     * interface name set and that differs from the interface set for this
+     * {@code LinkProperties} an {@link IllegalArgumentException} will be thrown.
+     * The proper course is to add either un-named or properly named {@link RouteInfo}.
+     *
+     * @param route A {@link RouteInfo} to add to this object.
+     * @return {@code true} was added or updated, false otherwise.
+     */
+    public boolean addRoute(@NonNull RouteInfo route) {
+        String routeIface = route.getInterface();
+        if (routeIface != null && !routeIface.equals(mIfaceName)) {
+            throw new IllegalArgumentException(
+                    "Route added with non-matching interface: " + routeIface
+                            + " vs. " + mIfaceName);
+        }
+        route = routeWithInterface(route);
+
+        int i = findRouteIndexByRouteKey(route);
+        if (i == -1) {
+            // Route was not present. Add it.
+            mRoutes.add(route);
+            return true;
+        } else if (mRoutes.get(i).equals(route)) {
+            // Route was present and has same properties. Do nothing.
+            return false;
+        } else {
+            // Route was present and has different properties. Update it.
+            mRoutes.set(i, route);
+            return true;
+        }
+    }
+
+    /**
+     * Removes a {@link RouteInfo} from this {@code LinkProperties}, if present. The route must
+     * specify an interface and the interface must match the interface of this
+     * {@code LinkProperties}, or it will not be removed.
+     *
+     * @param route A {@link RouteInfo} specifying the route to remove.
+     * @return {@code true} if the route was removed, {@code false} if it was not present.
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean removeRoute(@NonNull RouteInfo route) {
+        return Objects.equals(mIfaceName, route.getInterface()) && mRoutes.remove(route);
+    }
+
+    /**
+     * Returns all the {@link RouteInfo} set on this link.
+     *
+     * @return An unmodifiable {@link List} of {@link RouteInfo} for this link.
+     */
+    public @NonNull List<RouteInfo> getRoutes() {
+        return Collections.unmodifiableList(mRoutes);
+    }
+
+    /**
+     * Make sure this LinkProperties instance contains routes that cover the local subnet
+     * of its link addresses. Add any route that is missing.
+     * @hide
+     */
+    public void ensureDirectlyConnectedRoutes() {
+        for (LinkAddress addr : mLinkAddresses) {
+            addRoute(new RouteInfo(addr, null, mIfaceName));
+        }
+    }
+
+    /**
+     * Returns all the routes on this link and all the links stacked above it.
+     * @hide
+     */
+    @SystemApi
+    public @NonNull List<RouteInfo> getAllRoutes() {
+        List<RouteInfo> routes = new ArrayList<>(mRoutes);
+        for (LinkProperties stacked: mStackedLinks.values()) {
+            routes.addAll(stacked.getAllRoutes());
+        }
+        return routes;
+    }
+
+    /**
+     * Sets the recommended {@link ProxyInfo} to use on this link, or {@code null} for none.
+     * Note that Http Proxies are only a hint - the system recommends their use, but it does
+     * not enforce it and applications may ignore them.
+     *
+     * @param proxy A {@link ProxyInfo} defining the HTTP Proxy to use on this link.
+     */
+    public void setHttpProxy(@Nullable ProxyInfo proxy) {
+        mHttpProxy = proxy;
+    }
+
+    /**
+     * Gets the recommended {@link ProxyInfo} (or {@code null}) set on this link.
+     *
+     * @return The {@link ProxyInfo} set on this link or {@code null}.
+     */
+    public @Nullable ProxyInfo getHttpProxy() {
+        return mHttpProxy;
+    }
+
+    /**
+     * Returns the NAT64 prefix in use on this link, if any.
+     *
+     * @return the NAT64 prefix or {@code null}.
+     */
+    public @Nullable IpPrefix getNat64Prefix() {
+        return mNat64Prefix;
+    }
+
+    /**
+     * Sets the NAT64 prefix in use on this link.
+     *
+     * Currently, only 96-bit prefixes (i.e., where the 32-bit IPv4 address is at the end of the
+     * 128-bit IPv6 address) are supported or {@code null} for no prefix.
+     *
+     * @param prefix the NAT64 prefix.
+     */
+    public void setNat64Prefix(@Nullable IpPrefix prefix) {
+        if (prefix != null && prefix.getPrefixLength() != 96) {
+            throw new IllegalArgumentException("Only 96-bit prefixes are supported: " + prefix);
+        }
+        mNat64Prefix = prefix;  // IpPrefix objects are immutable.
+    }
+
+    /**
+     * Adds a stacked link.
+     *
+     * If there is already a stacked link with the same interface name as link,
+     * that link is replaced with link. Otherwise, link is added to the list
+     * of stacked links.
+     *
+     * @param link The link to add.
+     * @return true if the link was stacked, false otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean addStackedLink(@NonNull LinkProperties link) {
+        if (link.getInterfaceName() != null) {
+            mStackedLinks.put(link.getInterfaceName(), link);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes a stacked link.
+     *
+     * If there is a stacked link with the given interface name, it is
+     * removed. Otherwise, nothing changes.
+     *
+     * @param iface The interface name of the link to remove.
+     * @return true if the link was removed, false otherwise.
+     * @hide
+     */
+    public boolean removeStackedLink(@NonNull String iface) {
+        LinkProperties removed = mStackedLinks.remove(iface);
+        return removed != null;
+    }
+
+    /**
+     * Returns all the links stacked on top of this link.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public @NonNull List<LinkProperties> getStackedLinks() {
+        if (mStackedLinks.isEmpty()) {
+            return Collections.emptyList();
+        }
+        final List<LinkProperties> stacked = new ArrayList<>();
+        for (LinkProperties link : mStackedLinks.values()) {
+            stacked.add(new LinkProperties(link));
+        }
+        return Collections.unmodifiableList(stacked);
+    }
+
+    /**
+     * Clears this object to its initial state.
+     */
+    public void clear() {
+        if (mParcelSensitiveFields) {
+            throw new UnsupportedOperationException(
+                    "Cannot clear LinkProperties when parcelSensitiveFields is set");
+        }
+
+        mIfaceName = null;
+        mLinkAddresses.clear();
+        mDnses.clear();
+        mUsePrivateDns = false;
+        mPrivateDnsServerName = null;
+        mPcscfs.clear();
+        mDomains = null;
+        mRoutes.clear();
+        mHttpProxy = null;
+        mStackedLinks.clear();
+        mMtu = 0;
+        mDhcpServerAddress = null;
+        mTcpBufferSizes = null;
+        mNat64Prefix = null;
+        mWakeOnLanSupported = false;
+        mCaptivePortalApiUrl = null;
+        mCaptivePortalData = null;
+    }
+
+    /**
+     * Implement the Parcelable interface
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        // Space as a separator, so no need for spaces at start/end of the individual fragments.
+        final StringJoiner resultJoiner = new StringJoiner(" ", "{", "}");
+
+        if (mIfaceName != null) {
+            resultJoiner.add("InterfaceName:");
+            resultJoiner.add(mIfaceName);
+        }
+
+        resultJoiner.add("LinkAddresses: [");
+        if (!mLinkAddresses.isEmpty()) {
+            resultJoiner.add(TextUtils.join(",", mLinkAddresses));
+        }
+        resultJoiner.add("]");
+
+        resultJoiner.add("DnsAddresses: [");
+        if (!mDnses.isEmpty()) {
+            resultJoiner.add(TextUtils.join(",", mDnses));
+        }
+        resultJoiner.add("]");
+
+        if (mUsePrivateDns) {
+            resultJoiner.add("UsePrivateDns: true");
+        }
+
+        if (mPrivateDnsServerName != null) {
+            resultJoiner.add("PrivateDnsServerName:");
+            resultJoiner.add(mPrivateDnsServerName);
+        }
+
+        if (!mPcscfs.isEmpty()) {
+            resultJoiner.add("PcscfAddresses: [");
+            resultJoiner.add(TextUtils.join(",", mPcscfs));
+            resultJoiner.add("]");
+        }
+
+        if (!mValidatedPrivateDnses.isEmpty()) {
+            final StringJoiner validatedPrivateDnsesJoiner =
+                    new StringJoiner(",", "ValidatedPrivateDnsAddresses: [", "]");
+            for (final InetAddress addr : mValidatedPrivateDnses) {
+                validatedPrivateDnsesJoiner.add(addr.getHostAddress());
+            }
+            resultJoiner.add(validatedPrivateDnsesJoiner.toString());
+        }
+
+        resultJoiner.add("Domains:");
+        resultJoiner.add(mDomains);
+
+        resultJoiner.add("MTU:");
+        resultJoiner.add(Integer.toString(mMtu));
+
+        if (mWakeOnLanSupported) {
+            resultJoiner.add("WakeOnLanSupported: true");
+        }
+
+        if (mDhcpServerAddress != null) {
+            resultJoiner.add("ServerAddress:");
+            resultJoiner.add(mDhcpServerAddress.toString());
+        }
+
+        if (mCaptivePortalApiUrl != null) {
+            resultJoiner.add("CaptivePortalApiUrl: " + mCaptivePortalApiUrl);
+        }
+
+        if (mCaptivePortalData != null) {
+            resultJoiner.add("CaptivePortalData: " + mCaptivePortalData);
+        }
+
+        if (mTcpBufferSizes != null) {
+            resultJoiner.add("TcpBufferSizes:");
+            resultJoiner.add(mTcpBufferSizes);
+        }
+
+        resultJoiner.add("Routes: [");
+        if (!mRoutes.isEmpty()) {
+            resultJoiner.add(TextUtils.join(",", mRoutes));
+        }
+        resultJoiner.add("]");
+
+        if (mHttpProxy != null) {
+            resultJoiner.add("HttpProxy:");
+            resultJoiner.add(mHttpProxy.toString());
+        }
+
+        if (mNat64Prefix != null) {
+            resultJoiner.add("Nat64Prefix:");
+            resultJoiner.add(mNat64Prefix.toString());
+        }
+
+        final Collection<LinkProperties> stackedLinksValues = mStackedLinks.values();
+        if (!stackedLinksValues.isEmpty()) {
+            final StringJoiner stackedLinksJoiner = new StringJoiner(",", "Stacked: [", "]");
+            for (final LinkProperties lp : stackedLinksValues) {
+                stackedLinksJoiner.add("[ " + lp + " ]");
+            }
+            resultJoiner.add(stackedLinksJoiner.toString());
+        }
+
+        return resultJoiner.toString();
+    }
+
+    /**
+     * Returns true if this link has an IPv4 address.
+     *
+     * @return {@code true} if there is an IPv4 address, {@code false} otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean hasIpv4Address() {
+        for (LinkAddress address : mLinkAddresses) {
+            if (address.getAddress() instanceof Inet4Address) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if there is an IPv4 address, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean hasIPv4Address() {
+        return hasIpv4Address();
+    }
+
+    /**
+     * Returns true if this link or any of its stacked interfaces has an IPv4 address.
+     *
+     * @return {@code true} if there is an IPv4 address, {@code false} otherwise.
+     */
+    private boolean hasIpv4AddressOnInterface(String iface) {
+        // mIfaceName can be null.
+        return (Objects.equals(iface, mIfaceName) && hasIpv4Address())
+                || (iface != null && mStackedLinks.containsKey(iface)
+                        && mStackedLinks.get(iface).hasIpv4Address());
+    }
+
+    /**
+     * Returns true if this link has a global preferred IPv6 address.
+     *
+     * @return {@code true} if there is a global preferred IPv6 address, {@code false} otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean hasGlobalIpv6Address() {
+        for (LinkAddress address : mLinkAddresses) {
+          if (address.getAddress() instanceof Inet6Address && address.isGlobalPreferred()) {
+            return true;
+          }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if this link has an IPv4 unreachable default route.
+     *
+     * @return {@code true} if there is an IPv4 unreachable default route, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasIpv4UnreachableDefaultRoute() {
+        for (RouteInfo r : mRoutes) {
+            if (r.isIPv4UnreachableDefault()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if there is a global preferred IPv6 address, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean hasGlobalIPv6Address() {
+        return hasGlobalIpv6Address();
+    }
+
+    /**
+     * Returns true if this link has an IPv4 default route.
+     *
+     * @return {@code true} if there is an IPv4 default route, {@code false} otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean hasIpv4DefaultRoute() {
+        for (RouteInfo r : mRoutes) {
+            if (r.isIPv4Default()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if this link has an IPv6 unreachable default route.
+     *
+     * @return {@code true} if there is an IPv6 unreachable default route, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasIpv6UnreachableDefaultRoute() {
+        for (RouteInfo r : mRoutes) {
+            if (r.isIPv6UnreachableDefault()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if there is an IPv4 default route, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean hasIPv4DefaultRoute() {
+        return hasIpv4DefaultRoute();
+    }
+
+    /**
+     * Returns true if this link has an IPv6 default route.
+     *
+     * @return {@code true} if there is an IPv6 default route, {@code false} otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean hasIpv6DefaultRoute() {
+        for (RouteInfo r : mRoutes) {
+            if (r.isIPv6Default()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if there is an IPv6 default route, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean hasIPv6DefaultRoute() {
+        return hasIpv6DefaultRoute();
+    }
+
+    /**
+     * Returns true if this link has an IPv4 DNS server.
+     *
+     * @return {@code true} if there is an IPv4 DNS server, {@code false} otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean hasIpv4DnsServer() {
+        for (InetAddress ia : mDnses) {
+            if (ia instanceof Inet4Address) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if there is an IPv4 DNS server, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean hasIPv4DnsServer() {
+        return hasIpv4DnsServer();
+    }
+
+    /**
+     * Returns true if this link has an IPv6 DNS server.
+     *
+     * @return {@code true} if there is an IPv6 DNS server, {@code false} otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean hasIpv6DnsServer() {
+        for (InetAddress ia : mDnses) {
+            if (ia instanceof Inet6Address) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if there is an IPv6 DNS server, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean hasIPv6DnsServer() {
+        return hasIpv6DnsServer();
+    }
+
+    /**
+     * Returns true if this link has an IPv4 PCSCF server.
+     *
+     * @return {@code true} if there is an IPv4 PCSCF server, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasIpv4PcscfServer() {
+        for (InetAddress ia : mPcscfs) {
+          if (ia instanceof Inet4Address) {
+            return true;
+          }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if this link has an IPv6 PCSCF server.
+     *
+     * @return {@code true} if there is an IPv6 PCSCF server, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasIpv6PcscfServer() {
+        for (InetAddress ia : mPcscfs) {
+          if (ia instanceof Inet6Address) {
+            return true;
+          }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if this link is provisioned for global IPv4 connectivity.
+     * This requires an IP address, default route, and DNS server.
+     *
+     * @return {@code true} if the link is provisioned, {@code false} otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean isIpv4Provisioned() {
+        return (hasIpv4Address()
+                && hasIpv4DefaultRoute()
+                && hasIpv4DnsServer());
+    }
+
+    /**
+     * Returns true if this link is provisioned for global IPv6 connectivity.
+     * This requires an IP address, default route, and DNS server.
+     *
+     * @return {@code true} if the link is provisioned, {@code false} otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean isIpv6Provisioned() {
+        return (hasGlobalIpv6Address()
+                && hasIpv6DefaultRoute()
+                && hasIpv6DnsServer());
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if the link is provisioned, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean isIPv6Provisioned() {
+        return isIpv6Provisioned();
+    }
+
+
+    /**
+     * Returns true if this link is provisioned for global connectivity,
+     * for at least one Internet Protocol family.
+     *
+     * @return {@code true} if the link is provisioned, {@code false} otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean isProvisioned() {
+        return (isIpv4Provisioned() || isIpv6Provisioned());
+    }
+
+    /**
+     * Evaluate whether the {@link InetAddress} is considered reachable.
+     *
+     * @return {@code true} if the given {@link InetAddress} is considered reachable,
+     *         {@code false} otherwise.
+     * @hide
+     */
+    @SystemApi
+    public boolean isReachable(@NonNull InetAddress ip) {
+        final List<RouteInfo> allRoutes = getAllRoutes();
+        // If we don't have a route to this IP address, it's not reachable.
+        final RouteInfo bestRoute = RouteInfo.selectBestRoute(allRoutes, ip);
+        if (bestRoute == null) {
+            return false;
+        }
+
+        // TODO: better source address evaluation for destination addresses.
+
+        if (ip instanceof Inet4Address) {
+            // For IPv4, it suffices for now to simply have any address.
+            return hasIpv4AddressOnInterface(bestRoute.getInterface());
+        } else if (ip instanceof Inet6Address) {
+            if (ip.isLinkLocalAddress()) {
+                // For now, just make sure link-local destinations have
+                // scopedIds set, since transmits will generally fail otherwise.
+                // TODO: verify it matches the ifindex of one of the interfaces.
+                return (((Inet6Address)ip).getScopeId() != 0);
+            }  else {
+                // For non-link-local destinations check that either the best route
+                // is directly connected or that some global preferred address exists.
+                // TODO: reconsider all cases (disconnected ULA networks, ...).
+                return (!bestRoute.hasGateway() || hasGlobalIpv6Address());
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} interface name against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean isIdenticalInterfaceName(@NonNull LinkProperties target) {
+        return LinkPropertiesUtils.isIdenticalInterfaceName(target, this);
+    }
+
+    /**
+     * Compares this {@code LinkProperties} DHCP server address against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalDhcpServerAddress(@NonNull LinkProperties target) {
+        return Objects.equals(mDhcpServerAddress, target.mDhcpServerAddress);
+    }
+
+    /**
+     * Compares this {@code LinkProperties} interface addresses against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean isIdenticalAddresses(@NonNull LinkProperties target) {
+        return LinkPropertiesUtils.isIdenticalAddresses(target, this);
+    }
+
+    /**
+     * Compares this {@code LinkProperties} DNS addresses against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean isIdenticalDnses(@NonNull LinkProperties target) {
+        return LinkPropertiesUtils.isIdenticalDnses(target, this);
+    }
+
+    /**
+     * Compares this {@code LinkProperties} private DNS settings against the
+     * target.
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalPrivateDns(@NonNull LinkProperties target) {
+        return (isPrivateDnsActive() == target.isPrivateDnsActive()
+                && TextUtils.equals(getPrivateDnsServerName(),
+                target.getPrivateDnsServerName()));
+    }
+
+    /**
+     * Compares this {@code LinkProperties} validated private DNS addresses against
+     * the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalValidatedPrivateDnses(@NonNull LinkProperties target) {
+        Collection<InetAddress> targetDnses = target.getValidatedPrivateDnsServers();
+        return (mValidatedPrivateDnses.size() == targetDnses.size())
+                ? mValidatedPrivateDnses.containsAll(targetDnses) : false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} PCSCF addresses against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalPcscfs(@NonNull LinkProperties target) {
+        Collection<InetAddress> targetPcscfs = target.getPcscfServers();
+        return (mPcscfs.size() == targetPcscfs.size()) ?
+                    mPcscfs.containsAll(targetPcscfs) : false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} Routes against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean isIdenticalRoutes(@NonNull LinkProperties target) {
+        return LinkPropertiesUtils.isIdenticalRoutes(target, this);
+    }
+
+    /**
+     * Compares this {@code LinkProperties} HttpProxy against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public boolean isIdenticalHttpProxy(@NonNull LinkProperties target) {
+        return LinkPropertiesUtils.isIdenticalHttpProxy(target, this);
+    }
+
+    /**
+     * Compares this {@code LinkProperties} stacked links against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public boolean isIdenticalStackedLinks(@NonNull LinkProperties target) {
+        if (!mStackedLinks.keySet().equals(target.mStackedLinks.keySet())) {
+            return false;
+        }
+        for (LinkProperties stacked : mStackedLinks.values()) {
+            // Hashtable values can never be null.
+            String iface = stacked.getInterfaceName();
+            if (!stacked.equals(target.mStackedLinks.get(iface))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} MTU against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalMtu(@NonNull LinkProperties target) {
+        return getMtu() == target.getMtu();
+    }
+
+    /**
+     * Compares this {@code LinkProperties} Tcp buffer sizes against the target.
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalTcpBufferSizes(@NonNull LinkProperties target) {
+        return Objects.equals(mTcpBufferSizes, target.mTcpBufferSizes);
+    }
+
+    /**
+     * Compares this {@code LinkProperties} NAT64 prefix against the target.
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalNat64Prefix(@NonNull LinkProperties target) {
+        return Objects.equals(mNat64Prefix, target.mNat64Prefix);
+    }
+
+    /**
+     * Compares this {@code LinkProperties} WakeOnLan supported against the target.
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalWakeOnLan(LinkProperties target) {
+        return isWakeOnLanSupported() == target.isWakeOnLanSupported();
+    }
+
+    /**
+     * Compares this {@code LinkProperties}'s CaptivePortalApiUrl against the target.
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalCaptivePortalApiUrl(LinkProperties target) {
+        return Objects.equals(mCaptivePortalApiUrl, target.mCaptivePortalApiUrl);
+    }
+
+    /**
+     * Compares this {@code LinkProperties}'s CaptivePortalData against the target.
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalCaptivePortalData(LinkProperties target) {
+        return Objects.equals(mCaptivePortalData, target.mCaptivePortalData);
+    }
+
+    /**
+     * Set whether the network interface supports WakeOnLAN
+     *
+     * @param supported WakeOnLAN supported value
+     *
+     * @hide
+     */
+    public void setWakeOnLanSupported(boolean supported) {
+        mWakeOnLanSupported = supported;
+    }
+
+    /**
+     * Returns whether the network interface supports WakeOnLAN
+     *
+     * @return {@code true} if interface supports WakeOnLAN, {@code false} otherwise.
+     */
+    public boolean isWakeOnLanSupported() {
+        return mWakeOnLanSupported;
+    }
+
+    /**
+     * Set the URL of the captive portal API endpoint to get more information about the network.
+     * @hide
+     */
+    @SystemApi
+    public void setCaptivePortalApiUrl(@Nullable Uri url) {
+        mCaptivePortalApiUrl = url;
+    }
+
+    /**
+     * Get the URL of the captive portal API endpoint to get more information about the network.
+     *
+     * <p>This is null unless the application has
+     * {@link android.Manifest.permission.NETWORK_SETTINGS} or
+     * {@link NetworkStack#PERMISSION_MAINLINE_NETWORK_STACK} permissions, and the network provided
+     * the URL.
+     * @hide
+     */
+    @SystemApi
+    @Nullable
+    public Uri getCaptivePortalApiUrl() {
+        return mCaptivePortalApiUrl;
+    }
+
+    /**
+     * Set the CaptivePortalData obtained from the captive portal API (RFC7710bis).
+     * @hide
+     */
+    @SystemApi
+    public void setCaptivePortalData(@Nullable CaptivePortalData data) {
+        mCaptivePortalData = data;
+    }
+
+    /**
+     * Get the CaptivePortalData obtained from the captive portal API (RFC7710bis).
+     *
+     * <p>This is null unless the application has
+     * {@link android.Manifest.permission.NETWORK_SETTINGS} or
+     * {@link NetworkStack#PERMISSION_MAINLINE_NETWORK_STACK} permissions.
+     * @hide
+     */
+    @SystemApi
+    @Nullable
+    public CaptivePortalData getCaptivePortalData() {
+        return mCaptivePortalData;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} instance against the target
+     * LinkProperties in {@code obj}. Two LinkPropertieses are equal if
+     * all their fields are equal in values.
+     *
+     * For collection fields, such as mDnses, containsAll() is used to check
+     * if two collections contains the same elements, independent of order.
+     * There are two thoughts regarding containsAll()
+     * 1. Duplicated elements. eg, (A, B, B) and (A, A, B) are equal.
+     * 2. Worst case performance is O(n^2).
+     *
+     * @param obj the object to be tested for equality.
+     * @return {@code true} if both objects are equal, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof LinkProperties)) return false;
+
+        LinkProperties target = (LinkProperties) obj;
+        /*
+         * This method does not check that stacked interfaces are equal, because
+         * stacked interfaces are not so much a property of the link as a
+         * description of connections between links.
+         */
+        return isIdenticalInterfaceName(target)
+                && isIdenticalAddresses(target)
+                && isIdenticalDhcpServerAddress(target)
+                && isIdenticalDnses(target)
+                && isIdenticalPrivateDns(target)
+                && isIdenticalValidatedPrivateDnses(target)
+                && isIdenticalPcscfs(target)
+                && isIdenticalRoutes(target)
+                && isIdenticalHttpProxy(target)
+                && isIdenticalStackedLinks(target)
+                && isIdenticalMtu(target)
+                && isIdenticalTcpBufferSizes(target)
+                && isIdenticalNat64Prefix(target)
+                && isIdenticalWakeOnLan(target)
+                && isIdenticalCaptivePortalApiUrl(target)
+                && isIdenticalCaptivePortalData(target);
+    }
+
+    /**
+     * Generate hashcode based on significant fields
+     *
+     * Equal objects must produce the same hash code, while unequal objects
+     * may have the same hash codes.
+     */
+    @Override
+    public int hashCode() {
+        return ((null == mIfaceName) ? 0 : mIfaceName.hashCode()
+                + mLinkAddresses.size() * 31
+                + mDnses.size() * 37
+                + mValidatedPrivateDnses.size() * 61
+                + ((null == mDomains) ? 0 : mDomains.hashCode())
+                + mRoutes.size() * 41
+                + ((null == mHttpProxy) ? 0 : mHttpProxy.hashCode())
+                + mStackedLinks.hashCode() * 47)
+                + mMtu * 51
+                + ((null == mTcpBufferSizes) ? 0 : mTcpBufferSizes.hashCode())
+                + (mUsePrivateDns ? 57 : 0)
+                + ((null == mDhcpServerAddress) ? 0 : mDhcpServerAddress.hashCode())
+                + mPcscfs.size() * 67
+                + ((null == mPrivateDnsServerName) ? 0 : mPrivateDnsServerName.hashCode())
+                + Objects.hash(mNat64Prefix)
+                + (mWakeOnLanSupported ? 71 : 0)
+                + Objects.hash(mCaptivePortalApiUrl, mCaptivePortalData);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(getInterfaceName());
+        dest.writeInt(mLinkAddresses.size());
+        for (LinkAddress linkAddress : mLinkAddresses) {
+            dest.writeParcelable(linkAddress, flags);
+        }
+
+        writeAddresses(dest, mDnses);
+        writeAddresses(dest, mValidatedPrivateDnses);
+        dest.writeBoolean(mUsePrivateDns);
+        dest.writeString(mPrivateDnsServerName);
+        writeAddresses(dest, mPcscfs);
+        dest.writeString(mDomains);
+        writeAddress(dest, mDhcpServerAddress);
+        dest.writeInt(mMtu);
+        dest.writeString(mTcpBufferSizes);
+        dest.writeInt(mRoutes.size());
+        for (RouteInfo route : mRoutes) {
+            dest.writeParcelable(route, flags);
+        }
+
+        if (mHttpProxy != null) {
+            dest.writeByte((byte)1);
+            dest.writeParcelable(mHttpProxy, flags);
+        } else {
+            dest.writeByte((byte)0);
+        }
+        dest.writeParcelable(mNat64Prefix, 0);
+
+        ArrayList<LinkProperties> stackedLinks = new ArrayList<>(mStackedLinks.values());
+        dest.writeList(stackedLinks);
+
+        dest.writeBoolean(mWakeOnLanSupported);
+        dest.writeParcelable(mParcelSensitiveFields ? mCaptivePortalApiUrl : null, 0);
+        dest.writeParcelable(mParcelSensitiveFields ? mCaptivePortalData : null, 0);
+    }
+
+    private static void writeAddresses(@NonNull Parcel dest, @NonNull List<InetAddress> list) {
+        dest.writeInt(list.size());
+        for (InetAddress d : list) {
+            writeAddress(dest, d);
+        }
+    }
+
+    private static void writeAddress(@NonNull Parcel dest, @Nullable InetAddress addr) {
+        byte[] addressBytes = (addr == null ? null : addr.getAddress());
+        dest.writeByteArray(addressBytes);
+        if (addr instanceof Inet6Address) {
+            final Inet6Address v6Addr = (Inet6Address) addr;
+            final boolean hasScopeId = v6Addr.getScopeId() != 0;
+            dest.writeBoolean(hasScopeId);
+            if (hasScopeId) dest.writeInt(v6Addr.getScopeId());
+        }
+    }
+
+    @Nullable
+    private static InetAddress readAddress(@NonNull Parcel p) throws UnknownHostException {
+        final byte[] addr = p.createByteArray();
+        if (addr == null) return null;
+
+        if (addr.length == INET6_ADDR_LENGTH) {
+            final boolean hasScopeId = p.readBoolean();
+            final int scopeId = hasScopeId ? p.readInt() : 0;
+            return Inet6Address.getByAddress(null /* host */, addr, scopeId);
+        }
+
+        return InetAddress.getByAddress(addr);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public static final @android.annotation.NonNull Creator<LinkProperties> CREATOR =
+        new Creator<LinkProperties>() {
+            public LinkProperties createFromParcel(Parcel in) {
+                LinkProperties netProp = new LinkProperties();
+
+                String iface = in.readString();
+                if (iface != null) {
+                    netProp.setInterfaceName(iface);
+                }
+                int addressCount = in.readInt();
+                for (int i = 0; i < addressCount; i++) {
+                    netProp.addLinkAddress(in.readParcelable(null));
+                }
+                addressCount = in.readInt();
+                for (int i = 0; i < addressCount; i++) {
+                    try {
+                        netProp.addDnsServer(readAddress(in));
+                    } catch (UnknownHostException e) { }
+                }
+                addressCount = in.readInt();
+                for (int i = 0; i < addressCount; i++) {
+                    try {
+                        netProp.addValidatedPrivateDnsServer(readAddress(in));
+                    } catch (UnknownHostException e) { }
+                }
+                netProp.setUsePrivateDns(in.readBoolean());
+                netProp.setPrivateDnsServerName(in.readString());
+                addressCount = in.readInt();
+                for (int i = 0; i < addressCount; i++) {
+                    try {
+                        netProp.addPcscfServer(readAddress(in));
+                    } catch (UnknownHostException e) { }
+                }
+                netProp.setDomains(in.readString());
+                try {
+                    netProp.setDhcpServerAddress((Inet4Address) InetAddress
+                            .getByAddress(in.createByteArray()));
+                } catch (UnknownHostException e) { }
+                netProp.setMtu(in.readInt());
+                netProp.setTcpBufferSizes(in.readString());
+                addressCount = in.readInt();
+                for (int i = 0; i < addressCount; i++) {
+                    netProp.addRoute(in.readParcelable(null));
+                }
+                if (in.readByte() == 1) {
+                    netProp.setHttpProxy(in.readParcelable(null));
+                }
+                netProp.setNat64Prefix(in.readParcelable(null));
+                ArrayList<LinkProperties> stackedLinks = new ArrayList<LinkProperties>();
+                in.readList(stackedLinks, LinkProperties.class.getClassLoader());
+                for (LinkProperties stackedLink: stackedLinks) {
+                    netProp.addStackedLink(stackedLink);
+                }
+                netProp.setWakeOnLanSupported(in.readBoolean());
+
+                netProp.setCaptivePortalApiUrl(in.readParcelable(null));
+                netProp.setCaptivePortalData(in.readParcelable(null));
+                return netProp;
+            }
+
+            public LinkProperties[] newArray(int size) {
+                return new LinkProperties[size];
+            }
+        };
+
+    /**
+     * Check the valid MTU range based on IPv4 or IPv6.
+     * @hide
+     */
+    public static boolean isValidMtu(int mtu, boolean ipv6) {
+        if (ipv6) {
+            return mtu >= MIN_MTU_V6 && mtu <= MAX_MTU;
+        } else {
+            return mtu >= MIN_MTU && mtu <= MAX_MTU;
+        }
+    }
+}
diff --git a/framework/src/android/net/MacAddress.java b/framework/src/android/net/MacAddress.java
new file mode 100644
index 0000000..26a504a
--- /dev/null
+++ b/framework/src/android/net/MacAddress.java
@@ -0,0 +1,400 @@
+/*
+ * 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.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.net.wifi.WifiInfo;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.net.module.util.MacAddressUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Representation of a MAC address.
+ *
+ * This class only supports 48 bits long addresses and does not support 64 bits long addresses.
+ * Instances of this class are immutable. This class provides implementations of hashCode()
+ * and equals() that make it suitable for use as keys in standard implementations of
+ * {@link java.util.Map}.
+ */
+public final class MacAddress implements Parcelable {
+
+    private static final int ETHER_ADDR_LEN = 6;
+    private static final byte[] ETHER_ADDR_BROADCAST = addr(0xff, 0xff, 0xff, 0xff, 0xff, 0xff);
+
+    /**
+     * The MacAddress representing the unique broadcast MAC address.
+     */
+    public static final MacAddress BROADCAST_ADDRESS = MacAddress.fromBytes(ETHER_ADDR_BROADCAST);
+
+    /**
+     * The MacAddress zero MAC address.
+     *
+     * <p>Not publicly exposed or treated specially since the OUI 00:00:00 is registered.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static final MacAddress ALL_ZEROS_ADDRESS = new MacAddress(0);
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "TYPE_" }, value = {
+            TYPE_UNKNOWN,
+            TYPE_UNICAST,
+            TYPE_MULTICAST,
+            TYPE_BROADCAST,
+    })
+    public @interface MacAddressType { }
+
+    /** @hide Indicates a MAC address of unknown type. */
+    public static final int TYPE_UNKNOWN = 0;
+    /** Indicates a MAC address is a unicast address. */
+    public static final int TYPE_UNICAST = 1;
+    /** Indicates a MAC address is a multicast address. */
+    public static final int TYPE_MULTICAST = 2;
+    /** Indicates a MAC address is the broadcast address. */
+    public static final int TYPE_BROADCAST = 3;
+
+    private static final long VALID_LONG_MASK = (1L << 48) - 1;
+    private static final long LOCALLY_ASSIGNED_MASK = MacAddress.fromString("2:0:0:0:0:0").mAddr;
+    private static final long MULTICAST_MASK = MacAddress.fromString("1:0:0:0:0:0").mAddr;
+    private static final long OUI_MASK = MacAddress.fromString("ff:ff:ff:0:0:0").mAddr;
+    private static final long NIC_MASK = MacAddress.fromString("0:0:0:ff:ff:ff").mAddr;
+    private static final MacAddress BASE_GOOGLE_MAC = MacAddress.fromString("da:a1:19:0:0:0");
+    /** Default wifi MAC address used for a special purpose **/
+    private static final MacAddress DEFAULT_MAC_ADDRESS =
+            MacAddress.fromString(WifiInfo.DEFAULT_MAC_ADDRESS);
+
+    // Internal representation of the MAC address as a single 8 byte long.
+    // The encoding scheme sets the two most significant bytes to 0. The 6 bytes of the
+    // MAC address are encoded in the 6 least significant bytes of the long, where the first
+    // byte of the array is mapped to the 3rd highest logical byte of the long, the second
+    // byte of the array is mapped to the 4th highest logical byte of the long, and so on.
+    private final long mAddr;
+
+    private MacAddress(long addr) {
+        mAddr = (VALID_LONG_MASK & addr);
+    }
+
+    /**
+     * Returns the type of this address.
+     *
+     * @return the int constant representing the MAC address type of this MacAddress.
+     */
+    public @MacAddressType int getAddressType() {
+        if (equals(BROADCAST_ADDRESS)) {
+            return TYPE_BROADCAST;
+        }
+        if ((mAddr & MULTICAST_MASK) != 0) {
+            return TYPE_MULTICAST;
+        }
+        return TYPE_UNICAST;
+    }
+
+    /**
+     * @return true if this MacAddress is a locally assigned address.
+     */
+    public boolean isLocallyAssigned() {
+        return (mAddr & LOCALLY_ASSIGNED_MASK) != 0;
+    }
+
+    /**
+     * Convert this MacAddress to a byte array.
+     *
+     * The returned array is in network order. For example, if this MacAddress is 1:2:3:4:5:6,
+     * the returned array is [1, 2, 3, 4, 5, 6].
+     *
+     * @return a byte array representation of this MacAddress.
+     */
+    public @NonNull byte[] toByteArray() {
+        return byteAddrFromLongAddr(mAddr);
+    }
+
+    /**
+     * Returns a human-readable representation of this MacAddress.
+     * The exact format is implementation-dependent and should not be assumed to have any
+     * particular format.
+     */
+    @Override
+    public @NonNull String toString() {
+        return stringAddrFromLongAddr(mAddr);
+    }
+
+    /**
+     * @return a String representation of the OUI part of this MacAddress made of 3 hexadecimal
+     * numbers in [0,ff] joined by ':' characters.
+     */
+    public @NonNull String toOuiString() {
+        return String.format(
+                "%02x:%02x:%02x", (mAddr >> 40) & 0xff, (mAddr >> 32) & 0xff, (mAddr >> 24) & 0xff);
+    }
+
+    @Override
+    public int hashCode() {
+        return (int) ((mAddr >> 32) ^ mAddr);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        return (o instanceof MacAddress) && ((MacAddress) o).mAddr == mAddr;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeLong(mAddr);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<MacAddress> CREATOR =
+            new Parcelable.Creator<MacAddress>() {
+                public MacAddress createFromParcel(Parcel in) {
+                    return new MacAddress(in.readLong());
+                }
+
+                public MacAddress[] newArray(int size) {
+                    return new MacAddress[size];
+                }
+            };
+
+    /**
+     * Returns true if the given byte array is an valid MAC address.
+     * A valid byte array representation for a MacAddress is a non-null array of length 6.
+     *
+     * @param addr a byte array.
+     * @return true if the given byte array is not null and has the length of a MAC address.
+     *
+     * @hide
+     */
+    public static boolean isMacAddress(byte[] addr) {
+        return MacAddressUtils.isMacAddress(addr);
+    }
+
+    /**
+     * Returns the MAC address type of the MAC address represented by the given byte array,
+     * or null if the given byte array does not represent a MAC address.
+     * A valid byte array representation for a MacAddress is a non-null array of length 6.
+     *
+     * @param addr a byte array representing a MAC address.
+     * @return the int constant representing the MAC address type of the MAC address represented
+     * by the given byte array, or type UNKNOWN if the byte array is not a valid MAC address.
+     *
+     * @hide
+     */
+    public static int macAddressType(byte[] addr) {
+        if (!isMacAddress(addr)) {
+            return TYPE_UNKNOWN;
+        }
+        return MacAddress.fromBytes(addr).getAddressType();
+    }
+
+    /**
+     * Converts a String representation of a MAC address to a byte array representation.
+     * A valid String representation for a MacAddress is a series of 6 values in the
+     * range [0,ff] printed in hexadecimal and joined by ':' characters.
+     *
+     * @param addr a String representation of a MAC address.
+     * @return the byte representation of the MAC address.
+     * @throws IllegalArgumentException if the given String is not a valid representation.
+     *
+     * @hide
+     */
+    public static @NonNull byte[] byteAddrFromStringAddr(String addr) {
+        Objects.requireNonNull(addr);
+        String[] parts = addr.split(":");
+        if (parts.length != ETHER_ADDR_LEN) {
+            throw new IllegalArgumentException(addr + " was not a valid MAC address");
+        }
+        byte[] bytes = new byte[ETHER_ADDR_LEN];
+        for (int i = 0; i < ETHER_ADDR_LEN; i++) {
+            int x = Integer.valueOf(parts[i], 16);
+            if (x < 0 || 0xff < x) {
+                throw new IllegalArgumentException(addr + "was not a valid MAC address");
+            }
+            bytes[i] = (byte) x;
+        }
+        return bytes;
+    }
+
+    /**
+     * Converts a byte array representation of a MAC address to a String representation made
+     * of 6 hexadecimal numbers in [0,ff] joined by ':' characters.
+     * A valid byte array representation for a MacAddress is a non-null array of length 6.
+     *
+     * @param addr a byte array representation of a MAC address.
+     * @return the String representation of the MAC address.
+     * @throws IllegalArgumentException if the given byte array is not a valid representation.
+     *
+     * @hide
+     */
+    public static @NonNull String stringAddrFromByteAddr(byte[] addr) {
+        if (!isMacAddress(addr)) {
+            return null;
+        }
+        return String.format("%02x:%02x:%02x:%02x:%02x:%02x",
+                addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
+    }
+
+    private static byte[] byteAddrFromLongAddr(long addr) {
+        return MacAddressUtils.byteAddrFromLongAddr(addr);
+    }
+
+    private static long longAddrFromByteAddr(byte[] addr) {
+        return MacAddressUtils.longAddrFromByteAddr(addr);
+    }
+
+    // Internal conversion function equivalent to longAddrFromByteAddr(byteAddrFromStringAddr(addr))
+    // that avoids the allocation of an intermediary byte[].
+    private static long longAddrFromStringAddr(String addr) {
+        Objects.requireNonNull(addr);
+        String[] parts = addr.split(":");
+        if (parts.length != ETHER_ADDR_LEN) {
+            throw new IllegalArgumentException(addr + " was not a valid MAC address");
+        }
+        long longAddr = 0;
+        for (int i = 0; i < parts.length; i++) {
+            int x = Integer.valueOf(parts[i], 16);
+            if (x < 0 || 0xff < x) {
+                throw new IllegalArgumentException(addr + "was not a valid MAC address");
+            }
+            longAddr = x + (longAddr << 8);
+        }
+        return longAddr;
+    }
+
+    // Internal conversion function equivalent to stringAddrFromByteAddr(byteAddrFromLongAddr(addr))
+    // that avoids the allocation of an intermediary byte[].
+    private static @NonNull String stringAddrFromLongAddr(long addr) {
+        return String.format("%02x:%02x:%02x:%02x:%02x:%02x",
+                (addr >> 40) & 0xff,
+                (addr >> 32) & 0xff,
+                (addr >> 24) & 0xff,
+                (addr >> 16) & 0xff,
+                (addr >> 8) & 0xff,
+                addr & 0xff);
+    }
+
+    /**
+     * Creates a MacAddress from the given String representation. A valid String representation
+     * for a MacAddress is a series of 6 values in the range [0,ff] printed in hexadecimal
+     * and joined by ':' characters.
+     *
+     * @param addr a String representation of a MAC address.
+     * @return the MacAddress corresponding to the given String representation.
+     * @throws IllegalArgumentException if the given String is not a valid representation.
+     */
+    public static @NonNull MacAddress fromString(@NonNull String addr) {
+        return new MacAddress(longAddrFromStringAddr(addr));
+    }
+
+    /**
+     * Creates a MacAddress from the given byte array representation.
+     * A valid byte array representation for a MacAddress is a non-null array of length 6.
+     *
+     * @param addr a byte array representation of a MAC address.
+     * @return the MacAddress corresponding to the given byte array representation.
+     * @throws IllegalArgumentException if the given byte array is not a valid representation.
+     */
+    public static @NonNull MacAddress fromBytes(@NonNull byte[] addr) {
+        return new MacAddress(longAddrFromByteAddr(addr));
+    }
+
+    /**
+     * Returns a generated MAC address whose 24 least significant bits constituting the
+     * NIC part of the address are randomly selected and has Google OUI base.
+     *
+     * The locally assigned bit is always set to 1. The multicast bit is always set to 0.
+     *
+     * @return a random locally assigned, unicast MacAddress with Google OUI.
+     *
+     * @hide
+     */
+    public static @NonNull MacAddress createRandomUnicastAddressWithGoogleBase() {
+        return MacAddressUtils.createRandomUnicastAddress(BASE_GOOGLE_MAC, new SecureRandom());
+    }
+
+    // Convenience function for working around the lack of byte literals.
+    private static byte[] addr(int... in) {
+        if (in.length != ETHER_ADDR_LEN) {
+            throw new IllegalArgumentException(Arrays.toString(in)
+                    + " was not an array with length equal to " + ETHER_ADDR_LEN);
+        }
+        byte[] out = new byte[ETHER_ADDR_LEN];
+        for (int i = 0; i < ETHER_ADDR_LEN; i++) {
+            out[i] = (byte) in[i];
+        }
+        return out;
+    }
+
+    /**
+     * Checks if this MAC Address matches the provided range.
+     *
+     * @param baseAddress MacAddress representing the base address to compare with.
+     * @param mask MacAddress representing the mask to use during comparison.
+     * @return true if this MAC Address matches the given range.
+     *
+     */
+    public boolean matches(@NonNull MacAddress baseAddress, @NonNull MacAddress mask) {
+        Objects.requireNonNull(baseAddress);
+        Objects.requireNonNull(mask);
+        return (mAddr & mask.mAddr) == (baseAddress.mAddr & mask.mAddr);
+    }
+
+    /**
+     * Create a link-local Inet6Address from the MAC address. The EUI-48 MAC address is converted
+     * to an EUI-64 MAC address per RFC 4291. The resulting EUI-64 is used to construct a link-local
+     * IPv6 address per RFC 4862.
+     *
+     * @return A link-local Inet6Address constructed from the MAC address.
+     */
+    public @Nullable Inet6Address getLinkLocalIpv6FromEui48Mac() {
+        byte[] macEui48Bytes = toByteArray();
+        byte[] addr = new byte[16];
+
+        addr[0] = (byte) 0xfe;
+        addr[1] = (byte) 0x80;
+        addr[8] = (byte) (macEui48Bytes[0] ^ (byte) 0x02); // flip the link-local bit
+        addr[9] = macEui48Bytes[1];
+        addr[10] = macEui48Bytes[2];
+        addr[11] = (byte) 0xff;
+        addr[12] = (byte) 0xfe;
+        addr[13] = macEui48Bytes[3];
+        addr[14] = macEui48Bytes[4];
+        addr[15] = macEui48Bytes[5];
+
+        try {
+            return Inet6Address.getByAddress(null, addr, 0);
+        } catch (UnknownHostException e) {
+            return null;
+        }
+    }
+}
diff --git a/framework/src/android/net/NattKeepalivePacketData.java b/framework/src/android/net/NattKeepalivePacketData.java
new file mode 100644
index 0000000..c4f8fc2
--- /dev/null
+++ b/framework/src/android/net/NattKeepalivePacketData.java
@@ -0,0 +1,144 @@
+/*
+ * 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;
+
+import static android.net.InvalidPacketException.ERROR_INVALID_IP_ADDRESS;
+import static android.net.InvalidPacketException.ERROR_INVALID_PORT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.system.OsConstants;
+
+import com.android.net.module.util.IpUtils;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Objects;
+
+/** @hide */
+@SystemApi
+public final class NattKeepalivePacketData extends KeepalivePacketData implements Parcelable {
+    private static final int IPV4_HEADER_LENGTH = 20;
+    private static final int UDP_HEADER_LENGTH = 8;
+
+    // This should only be constructed via static factory methods, such as
+    // nattKeepalivePacket
+    public NattKeepalivePacketData(@NonNull InetAddress srcAddress, int srcPort,
+            @NonNull InetAddress dstAddress, int dstPort, @NonNull byte[] data) throws
+            InvalidPacketException {
+        super(srcAddress, srcPort, dstAddress, dstPort, data);
+    }
+
+    /**
+     * Factory method to create Nat-T keepalive packet structure.
+     * @hide
+     */
+    public static NattKeepalivePacketData nattKeepalivePacket(
+            InetAddress srcAddress, int srcPort, InetAddress dstAddress, int dstPort)
+            throws InvalidPacketException {
+
+        if (!(srcAddress instanceof Inet4Address) || !(dstAddress instanceof Inet4Address)) {
+            throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+        }
+
+        if (dstPort != NattSocketKeepalive.NATT_PORT) {
+            throw new InvalidPacketException(ERROR_INVALID_PORT);
+        }
+
+        int length = IPV4_HEADER_LENGTH + UDP_HEADER_LENGTH + 1;
+        ByteBuffer buf = ByteBuffer.allocate(length);
+        buf.order(ByteOrder.BIG_ENDIAN);
+        buf.putShort((short) 0x4500);             // IP version and TOS
+        buf.putShort((short) length);
+        buf.putInt(0);                            // ID, flags, offset
+        buf.put((byte) 64);                       // TTL
+        buf.put((byte) OsConstants.IPPROTO_UDP);
+        int ipChecksumOffset = buf.position();
+        buf.putShort((short) 0);                  // IP checksum
+        buf.put(srcAddress.getAddress());
+        buf.put(dstAddress.getAddress());
+        buf.putShort((short) srcPort);
+        buf.putShort((short) dstPort);
+        buf.putShort((short) (length - 20));      // UDP length
+        int udpChecksumOffset = buf.position();
+        buf.putShort((short) 0);                  // UDP checksum
+        buf.put((byte) 0xff);                     // NAT-T keepalive
+        buf.putShort(ipChecksumOffset, IpUtils.ipChecksum(buf, 0));
+        buf.putShort(udpChecksumOffset, IpUtils.udpChecksum(buf, 0, IPV4_HEADER_LENGTH));
+
+        return new NattKeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, buf.array());
+    }
+
+    /** Parcelable Implementation */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Write to parcel */
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeString(getSrcAddress().getHostAddress());
+        out.writeString(getDstAddress().getHostAddress());
+        out.writeInt(getSrcPort());
+        out.writeInt(getDstPort());
+    }
+
+    /** Parcelable Creator */
+    public static final @NonNull Parcelable.Creator<NattKeepalivePacketData> CREATOR =
+            new Parcelable.Creator<NattKeepalivePacketData>() {
+                public NattKeepalivePacketData createFromParcel(Parcel in) {
+                    final InetAddress srcAddress =
+                            InetAddresses.parseNumericAddress(in.readString());
+                    final InetAddress dstAddress =
+                            InetAddresses.parseNumericAddress(in.readString());
+                    final int srcPort = in.readInt();
+                    final int dstPort = in.readInt();
+                    try {
+                        return NattKeepalivePacketData.nattKeepalivePacket(srcAddress, srcPort,
+                                    dstAddress, dstPort);
+                    } catch (InvalidPacketException e) {
+                        throw new IllegalArgumentException(
+                                "Invalid NAT-T keepalive data: " + e.getError());
+                    }
+                }
+
+                public NattKeepalivePacketData[] newArray(int size) {
+                    return new NattKeepalivePacketData[size];
+                }
+            };
+
+    @Override
+    public boolean equals(@Nullable final Object o) {
+        if (!(o instanceof NattKeepalivePacketData)) return false;
+        final NattKeepalivePacketData other = (NattKeepalivePacketData) o;
+        final InetAddress srcAddress = getSrcAddress();
+        final InetAddress dstAddress = getDstAddress();
+        return srcAddress.equals(other.getSrcAddress())
+            && dstAddress.equals(other.getDstAddress())
+            && getSrcPort() == other.getSrcPort()
+            && getDstPort() == other.getDstPort();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getSrcAddress(), getDstAddress(), getSrcPort(), getDstPort());
+    }
+}
diff --git a/framework/src/android/net/NattSocketKeepalive.java b/framework/src/android/net/NattSocketKeepalive.java
new file mode 100644
index 0000000..a15d165
--- /dev/null
+++ b/framework/src/android/net/NattSocketKeepalive.java
@@ -0,0 +1,77 @@
+/*
+ * 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.annotation.NonNull;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.net.InetAddress;
+import java.util.concurrent.Executor;
+
+/** @hide */
+public final class NattSocketKeepalive extends SocketKeepalive {
+    /** The NAT-T destination port for IPsec */
+    public static final int NATT_PORT = 4500;
+
+    @NonNull private final InetAddress mSource;
+    @NonNull private final InetAddress mDestination;
+    private final int mResourceId;
+
+    NattSocketKeepalive(@NonNull IConnectivityManager service,
+            @NonNull Network network,
+            @NonNull ParcelFileDescriptor pfd,
+            int resourceId,
+            @NonNull InetAddress source,
+            @NonNull InetAddress destination,
+            @NonNull Executor executor,
+            @NonNull Callback callback) {
+        super(service, network, pfd, executor, callback);
+        mSource = source;
+        mDestination = destination;
+        mResourceId = resourceId;
+    }
+
+    @Override
+    void startImpl(int intervalSec) {
+        mExecutor.execute(() -> {
+            try {
+                mService.startNattKeepaliveWithFd(mNetwork, mPfd, mResourceId,
+                        intervalSec, mCallback,
+                        mSource.getHostAddress(), mDestination.getHostAddress());
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error starting socket keepalive: ", e);
+                throw e.rethrowFromSystemServer();
+            }
+        });
+    }
+
+    @Override
+    void stopImpl() {
+        mExecutor.execute(() -> {
+            try {
+                if (mSlot != null) {
+                    mService.stopKeepalive(mNetwork, mSlot);
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error stopping socket keepalive: ", e);
+                throw e.rethrowFromSystemServer();
+            }
+        });
+    }
+}
diff --git a/framework/src/android/net/Network.java b/framework/src/android/net/Network.java
new file mode 100644
index 0000000..1f49033
--- /dev/null
+++ b/framework/src/android/net/Network.java
@@ -0,0 +1,530 @@
+/*
+ * 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.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.ParcelFileDescriptor;
+import android.os.Parcelable;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import com.android.internal.annotations.GuardedBy;
+
+import libcore.io.IoUtils;
+import libcore.net.http.Dns;
+import libcore.net.http.HttpURLConnectionFactory;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.SocketFactory;
+
+/**
+ * Identifies a {@code Network}.  This is supplied to applications via
+ * {@link ConnectivityManager.NetworkCallback} in response to the active
+ * {@link ConnectivityManager#requestNetwork} or passive
+ * {@link ConnectivityManager#registerNetworkCallback} calls.
+ * It is used to direct traffic to the given {@code Network}, either on a {@link Socket} basis
+ * through a targeted {@link SocketFactory} or process-wide via
+ * {@link ConnectivityManager#bindProcessToNetwork}.
+ */
+public class Network implements Parcelable {
+
+    /**
+     * The unique id of the network.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final int netId;
+
+    // Objects used to perform per-network operations such as getSocketFactory
+    // and openConnection, and a lock to protect access to them.
+    private volatile NetworkBoundSocketFactory mNetworkBoundSocketFactory = null;
+    // mUrlConnectionFactory is initialized lazily when it is first needed.
+    @GuardedBy("mLock")
+    private HttpURLConnectionFactory mUrlConnectionFactory;
+    private final Object mLock = new Object();
+
+    // Default connection pool values. These are evaluated at startup, just
+    // like the OkHttp code. Also like the OkHttp code, we will throw parse
+    // exceptions at class loading time if the properties are set but are not
+    // valid integers.
+    private static final boolean httpKeepAlive =
+            Boolean.parseBoolean(System.getProperty("http.keepAlive", "true"));
+    private static final int httpMaxConnections =
+            httpKeepAlive ? Integer.parseInt(System.getProperty("http.maxConnections", "5")) : 0;
+    private static final long httpKeepAliveDurationMs =
+            Long.parseLong(System.getProperty("http.keepAliveDuration", "300000"));  // 5 minutes.
+    // Value used to obfuscate network handle longs.
+    // The HANDLE_MAGIC value MUST be kept in sync with the corresponding
+    // value in the native/android/net.c NDK implementation.
+    private static final long HANDLE_MAGIC = 0xcafed00dL;
+    private static final int HANDLE_MAGIC_SIZE = 32;
+    private static final int USE_LOCAL_NAMESERVERS_FLAG = 0x80000000;
+
+    // A boolean to control how getAllByName()/getByName() behaves in the face
+    // of Private DNS.
+    //
+    // When true, these calls will request that DNS resolution bypass any
+    // Private DNS that might otherwise apply. Use of this feature is restricted
+    // and permission checks are made by netd (attempts to bypass Private DNS
+    // without appropriate permission are silently turned into vanilla DNS
+    // requests). This only affects DNS queries made using this network object.
+    //
+    // It it not parceled to receivers because (a) it can be set or cleared at
+    // anytime and (b) receivers should be explicit about attempts to bypass
+    // Private DNS so that the intent of the code is easily determined and
+    // code search audits are possible.
+    private final transient boolean mPrivateDnsBypass;
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public Network(int netId) {
+        this(netId, false);
+    }
+
+    /**
+     * @hide
+     */
+    public Network(int netId, boolean privateDnsBypass) {
+        this.netId = netId;
+        this.mPrivateDnsBypass = privateDnsBypass;
+    }
+
+    /**
+     * @hide
+     */
+    @SystemApi
+    public Network(@NonNull Network that) {
+        this(that.netId, that.mPrivateDnsBypass);
+    }
+
+    /**
+     * Operates the same as {@code InetAddress.getAllByName} except that host
+     * resolution is done on this network.
+     *
+     * @param host the hostname or literal IP string to be resolved.
+     * @return the array of addresses associated with the specified host.
+     * @throws UnknownHostException if the address lookup fails.
+     */
+    public InetAddress[] getAllByName(String host) throws UnknownHostException {
+        return InetAddressCompat.getAllByNameOnNet(host, getNetIdForResolv());
+    }
+
+    /**
+     * Operates the same as {@code InetAddress.getByName} except that host
+     * resolution is done on this network.
+     *
+     * @param host the hostname to be resolved to an address or {@code null}.
+     * @return the {@code InetAddress} instance representing the host.
+     * @throws UnknownHostException
+     *             if the address lookup fails.
+     */
+    public InetAddress getByName(String host) throws UnknownHostException {
+        return InetAddressCompat.getByNameOnNet(host, getNetIdForResolv());
+    }
+
+    /**
+     * Obtain a Network object for which Private DNS is to be bypassed when attempting
+     * to use {@link #getAllByName(String)}/{@link #getByName(String)} methods on the given
+     * instance for hostname resolution.
+     *
+     * @hide
+     */
+    @SystemApi
+    public @NonNull Network getPrivateDnsBypassingCopy() {
+        return new Network(netId, true);
+    }
+
+    /**
+     * Get the unique id of the network.
+     *
+     * @hide
+     */
+    @SystemApi
+    public int getNetId() {
+        return netId;
+    }
+
+    /**
+     * Returns a netid marked with the Private DNS bypass flag.
+     *
+     * This flag must be kept in sync with the NETID_USE_LOCAL_NAMESERVERS flag
+     * in system/netd/include/NetdClient.h.
+     *
+     * @hide
+     */
+    public int getNetIdForResolv() {
+        return mPrivateDnsBypass
+                ? (USE_LOCAL_NAMESERVERS_FLAG | netId)  // Non-portable DNS resolution flag.
+                : netId;
+    }
+
+    /**
+     * A {@code SocketFactory} that produces {@code Socket}'s bound to this network.
+     */
+    private class NetworkBoundSocketFactory extends SocketFactory {
+        private Socket connectToHost(String host, int port, SocketAddress localAddress)
+                throws IOException {
+            // Lookup addresses only on this Network.
+            InetAddress[] hostAddresses = getAllByName(host);
+            // Try all addresses.
+            for (int i = 0; i < hostAddresses.length; i++) {
+                try {
+                    Socket socket = createSocket();
+                    boolean failed = true;
+                    try {
+                        if (localAddress != null) socket.bind(localAddress);
+                        socket.connect(new InetSocketAddress(hostAddresses[i], port));
+                        failed = false;
+                        return socket;
+                    } finally {
+                        if (failed) IoUtils.closeQuietly(socket);
+                    }
+                } catch (IOException e) {
+                    if (i == (hostAddresses.length - 1)) throw e;
+                }
+            }
+            throw new UnknownHostException(host);
+        }
+
+        @Override
+        public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
+                throws IOException {
+            return connectToHost(host, port, new InetSocketAddress(localHost, localPort));
+        }
+
+        @Override
+        public Socket createSocket(InetAddress address, int port, InetAddress localAddress,
+                int localPort) throws IOException {
+            Socket socket = createSocket();
+            boolean failed = true;
+            try {
+                socket.bind(new InetSocketAddress(localAddress, localPort));
+                socket.connect(new InetSocketAddress(address, port));
+                failed = false;
+            } finally {
+                if (failed) IoUtils.closeQuietly(socket);
+            }
+            return socket;
+        }
+
+        @Override
+        public Socket createSocket(InetAddress host, int port) throws IOException {
+            Socket socket = createSocket();
+            boolean failed = true;
+            try {
+                socket.connect(new InetSocketAddress(host, port));
+                failed = false;
+            } finally {
+                if (failed) IoUtils.closeQuietly(socket);
+            }
+            return socket;
+        }
+
+        @Override
+        public Socket createSocket(String host, int port) throws IOException {
+            return connectToHost(host, port, null);
+        }
+
+        @Override
+        public Socket createSocket() throws IOException {
+            Socket socket = new Socket();
+            boolean failed = true;
+            try {
+                bindSocket(socket);
+                failed = false;
+            } finally {
+                if (failed) IoUtils.closeQuietly(socket);
+            }
+            return socket;
+        }
+    }
+
+    /**
+     * Returns a {@link SocketFactory} bound to this network.  Any {@link Socket} created by
+     * this factory will have its traffic sent over this {@code Network}.  Note that if this
+     * {@code Network} ever disconnects, this factory and any {@link Socket} it produced in the
+     * past or future will cease to work.
+     *
+     * @return a {@link SocketFactory} which produces {@link Socket} instances bound to this
+     *         {@code Network}.
+     */
+    public SocketFactory getSocketFactory() {
+        if (mNetworkBoundSocketFactory == null) {
+            synchronized (mLock) {
+                if (mNetworkBoundSocketFactory == null) {
+                    mNetworkBoundSocketFactory = new NetworkBoundSocketFactory();
+                }
+            }
+        }
+        return mNetworkBoundSocketFactory;
+    }
+
+    private static HttpURLConnectionFactory createUrlConnectionFactory(Dns dnsLookup) {
+        // Set configuration on the HttpURLConnectionFactory that will be good for all
+        // connections created by this Network. Configuration that might vary is left
+        // until openConnection() and passed as arguments.
+        HttpURLConnectionFactory urlConnectionFactory = HttpURLConnectionFactory.createInstance();
+        urlConnectionFactory.setDns(dnsLookup); // Let traffic go via dnsLookup
+        // A private connection pool just for this Network.
+        urlConnectionFactory.setNewConnectionPool(httpMaxConnections,
+                httpKeepAliveDurationMs, TimeUnit.MILLISECONDS);
+        return urlConnectionFactory;
+    }
+
+    /**
+     * Opens the specified {@link URL} on this {@code Network}, such that all traffic will be sent
+     * on this Network. The URL protocol must be {@code HTTP} or {@code HTTPS}.
+     *
+     * @return a {@code URLConnection} to the resource referred to by this URL.
+     * @throws MalformedURLException if the URL protocol is not HTTP or HTTPS.
+     * @throws IOException if an error occurs while opening the connection.
+     * @see java.net.URL#openConnection()
+     */
+    public URLConnection openConnection(URL url) throws IOException {
+        final ConnectivityManager cm = ConnectivityManager.getInstanceOrNull();
+        if (cm == null) {
+            throw new IOException("No ConnectivityManager yet constructed, please construct one");
+        }
+        // TODO: Should this be optimized to avoid fetching the global proxy for every request?
+        final ProxyInfo proxyInfo = cm.getProxyForNetwork(this);
+        final java.net.Proxy proxy;
+        if (proxyInfo != null) {
+            proxy = proxyInfo.makeProxy();
+        } else {
+            proxy = java.net.Proxy.NO_PROXY;
+        }
+        return openConnection(url, proxy);
+    }
+
+    /**
+     * Opens the specified {@link URL} on this {@code Network}, such that all traffic will be sent
+     * on this Network. The URL protocol must be {@code HTTP} or {@code HTTPS}.
+     *
+     * @param proxy the proxy through which the connection will be established.
+     * @return a {@code URLConnection} to the resource referred to by this URL.
+     * @throws MalformedURLException if the URL protocol is not HTTP or HTTPS.
+     * @throws IllegalArgumentException if the argument proxy is null.
+     * @throws IOException if an error occurs while opening the connection.
+     * @see java.net.URL#openConnection()
+     */
+    public URLConnection openConnection(URL url, java.net.Proxy proxy) throws IOException {
+        if (proxy == null) throw new IllegalArgumentException("proxy is null");
+        // TODO: This creates a connection pool and host resolver for
+        // every Network object, instead of one for every NetId. This is
+        // suboptimal, because an app could potentially have more than one
+        // Network object for the same NetId, causing increased memory footprint
+        // and performance penalties due to lack of connection reuse (connection
+        // setup time, congestion window growth time, etc.).
+        //
+        // Instead, investigate only having one connection pool and host resolver
+        // for every NetId, perhaps by using a static HashMap of NetIds to
+        // connection pools and host resolvers. The tricky part is deciding when
+        // to remove a map entry; a WeakHashMap shouldn't be used because whether
+        // a Network is referenced doesn't correlate with whether a new Network
+        // will be instantiated in the near future with the same NetID. A good
+        // solution would involve purging empty (or when all connections are timed
+        // out) ConnectionPools.
+        final HttpURLConnectionFactory urlConnectionFactory;
+        synchronized (mLock) {
+            if (mUrlConnectionFactory == null) {
+                Dns dnsLookup = hostname -> Arrays.asList(getAllByName(hostname));
+                mUrlConnectionFactory = createUrlConnectionFactory(dnsLookup);
+            }
+            urlConnectionFactory = mUrlConnectionFactory;
+        }
+        SocketFactory socketFactory = getSocketFactory();
+        return urlConnectionFactory.openConnection(url, socketFactory, proxy);
+    }
+
+    /**
+     * Binds the specified {@link DatagramSocket} to this {@code Network}. All data traffic on the
+     * socket will be sent on this {@code Network}, irrespective of any process-wide network binding
+     * set by {@link ConnectivityManager#bindProcessToNetwork}. The socket must not be
+     * connected.
+     */
+    public void bindSocket(DatagramSocket socket) throws IOException {
+        // Query a property of the underlying socket to ensure that the socket's file descriptor
+        // exists, is available to bind to a network and is not closed.
+        socket.getReuseAddress();
+        final ParcelFileDescriptor pfd = ParcelFileDescriptor.fromDatagramSocket(socket);
+        bindSocket(pfd.getFileDescriptor());
+        // ParcelFileDescriptor.fromSocket() creates a dup of the original fd. The original and the
+        // dup share the underlying socket in the kernel. The socket is never truly closed until the
+        // last fd pointing to the socket being closed. So close the dup one after binding the
+        // socket to control the lifetime of the dup fd.
+        pfd.close();
+    }
+
+    /**
+     * Binds the specified {@link Socket} to this {@code Network}. All data traffic on the socket
+     * will be sent on this {@code Network}, irrespective of any process-wide network binding set by
+     * {@link ConnectivityManager#bindProcessToNetwork}. The socket must not be connected.
+     */
+    public void bindSocket(Socket socket) throws IOException {
+        // Query a property of the underlying socket to ensure that the socket's file descriptor
+        // exists, is available to bind to a network and is not closed.
+        socket.getReuseAddress();
+        final ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket);
+        bindSocket(pfd.getFileDescriptor());
+        // ParcelFileDescriptor.fromSocket() creates a dup of the original fd. The original and the
+        // dup share the underlying socket in the kernel. The socket is never truly closed until the
+        // last fd pointing to the socket being closed. So close the dup one after binding the
+        // socket to control the lifetime of the dup fd.
+        pfd.close();
+    }
+
+    /**
+     * Binds the specified {@link FileDescriptor} to this {@code Network}. All data traffic on the
+     * socket represented by this file descriptor will be sent on this {@code Network},
+     * irrespective of any process-wide network binding set by
+     * {@link ConnectivityManager#bindProcessToNetwork}. The socket must not be connected.
+     */
+    public void bindSocket(FileDescriptor fd) throws IOException {
+        try {
+            final SocketAddress peer = Os.getpeername(fd);
+            final InetAddress inetPeer = ((InetSocketAddress) peer).getAddress();
+            if (!inetPeer.isAnyLocalAddress()) {
+                // Apparently, the kernel doesn't update a connected UDP socket's
+                // routing upon mark changes.
+                throw new SocketException("Socket is connected");
+            }
+        } catch (ErrnoException e) {
+            // getpeername() failed.
+            if (e.errno != OsConstants.ENOTCONN) {
+                throw e.rethrowAsSocketException();
+            }
+        } catch (ClassCastException e) {
+            // Wasn't an InetSocketAddress.
+            throw new SocketException("Only AF_INET/AF_INET6 sockets supported");
+        }
+
+        final int err = NetworkUtils.bindSocketToNetwork(fd, netId);
+        if (err != 0) {
+            // bindSocketToNetwork returns negative errno.
+            throw new ErrnoException("Binding socket to network " + netId, -err)
+                    .rethrowAsSocketException();
+        }
+    }
+
+    /**
+     * Returns a {@link Network} object given a handle returned from {@link #getNetworkHandle}.
+     *
+     * @param networkHandle a handle returned from {@link #getNetworkHandle}.
+     * @return A {@link Network} object derived from {@code networkHandle}.
+     */
+    public static Network fromNetworkHandle(long networkHandle) {
+        if (networkHandle == 0) {
+            throw new IllegalArgumentException(
+                    "Network.fromNetworkHandle refusing to instantiate NETID_UNSET Network.");
+        }
+        if ((networkHandle & ((1L << HANDLE_MAGIC_SIZE) - 1)) != HANDLE_MAGIC) {
+            throw new IllegalArgumentException(
+                    "Value passed to fromNetworkHandle() is not a network handle.");
+        }
+        final int netIdForResolv = (int) (networkHandle >>> HANDLE_MAGIC_SIZE);
+        return new Network((netIdForResolv & ~USE_LOCAL_NAMESERVERS_FLAG),
+                ((netIdForResolv & USE_LOCAL_NAMESERVERS_FLAG) != 0) /* privateDnsBypass */);
+    }
+
+    /**
+     * Returns a handle representing this {@code Network}, for use with the NDK API.
+     */
+    public long getNetworkHandle() {
+        // The network handle is explicitly not the same as the netId.
+        //
+        // The netId is an implementation detail which might be changed in the
+        // future, or which alone (i.e. in the absence of some additional
+        // context) might not be sufficient to fully identify a Network.
+        //
+        // As such, the intention is to prevent accidental misuse of the API
+        // that might result if a developer assumed that handles and netIds
+        // were identical and passing a netId to a call expecting a handle
+        // "just worked".  Such accidental misuse, if widely deployed, might
+        // prevent future changes to the semantics of the netId field or
+        // inhibit the expansion of state required for Network objects.
+        //
+        // This extra layer of indirection might be seen as paranoia, and might
+        // never end up being necessary, but the added complexity is trivial.
+        // At some future date it may be desirable to realign the handle with
+        // Multiple Provisioning Domains API recommendations, as made by the
+        // IETF mif working group.
+        if (netId == 0) {
+            return 0L;  // make this zero condition obvious for debugging
+        }
+        return (((long) getNetIdForResolv()) << HANDLE_MAGIC_SIZE) | HANDLE_MAGIC;
+    }
+
+    // implement the Parcelable interface
+    public int describeContents() {
+        return 0;
+    }
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(netId);
+    }
+
+    public static final @android.annotation.NonNull Creator<Network> CREATOR =
+        new Creator<Network>() {
+            public Network createFromParcel(Parcel in) {
+                int netId = in.readInt();
+
+                return new Network(netId);
+            }
+
+            public Network[] newArray(int size) {
+                return new Network[size];
+            }
+    };
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof Network)) return false;
+        Network other = (Network)obj;
+        return this.netId == other.netId;
+    }
+
+    @Override
+    public int hashCode() {
+        return netId * 11;
+    }
+
+    @Override
+    public String toString() {
+        return Integer.toString(netId);
+    }
+}
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
new file mode 100644
index 0000000..adcf338
--- /dev/null
+++ b/framework/src/android/net/NetworkAgent.java
@@ -0,0 +1,1324 @@
+/*
+ * 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.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.telephony.data.EpsBearerQosSessionAttributes;
+import android.telephony.data.NrQosSessionAttributes;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A utility class for handling for communicating between bearer-specific
+ * code and ConnectivityService.
+ *
+ * An agent manages the life cycle of a network. A network starts its
+ * life cycle when {@link register} is called on NetworkAgent. The network
+ * is then connecting. When full L3 connectivity has been established,
+ * the agent should call {@link markConnected} to inform the system that
+ * this network is ready to use. When the network disconnects its life
+ * ends and the agent should call {@link unregister}, at which point the
+ * system will clean up and free resources.
+ * Any reconnection becomes a new logical network, so after a network
+ * is disconnected the agent cannot be used any more. Network providers
+ * should create a new NetworkAgent instance to handle new connections.
+ *
+ * A bearer may have more than one NetworkAgent if it can simultaneously
+ * support separate networks (IMS / Internet / MMS Apns on cellular, or
+ * perhaps connections with different SSID or P2P for Wi-Fi).
+ *
+ * This class supports methods to start and stop sending keepalive packets.
+ * Keepalive packets are typically sent at periodic intervals over a network
+ * with NAT when there is no other traffic to avoid the network forcefully
+ * closing the connection. NetworkAgents that manage technologies that
+ * have hardware support for keepalive should implement the related
+ * methods to save battery life. NetworkAgent that cannot get support
+ * without waking up the CPU should not, as this would be prohibitive in
+ * terms of battery - these agents should simply not override the related
+ * methods, which results in the implementation returning
+ * {@link SocketKeepalive.ERROR_UNSUPPORTED} as appropriate.
+ *
+ * Keepalive packets need to be sent at relatively frequent intervals
+ * (a few seconds to a few minutes). As the contents of keepalive packets
+ * depend on the current network status, hardware needs to be configured
+ * to send them and has a limited amount of memory to do so. The HAL
+ * formalizes this as slots that an implementation can configure to send
+ * the correct packets. Devices typically have a small number of slots
+ * per radio technology, and the specific number of slots for each
+ * technology is specified in configuration files.
+ * {@see SocketKeepalive} for details.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class NetworkAgent {
+    /**
+     * The {@link Network} corresponding to this object.
+     */
+    @Nullable
+    private volatile Network mNetwork;
+
+    @Nullable
+    private volatile INetworkAgentRegistry mRegistry;
+
+    private interface RegistryAction {
+        void execute(@NonNull INetworkAgentRegistry registry) throws RemoteException;
+    }
+
+    private final Handler mHandler;
+    private final String LOG_TAG;
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+    /** @hide */
+    @TestApi
+    public static final int MIN_LINGER_TIMER_MS = 2000;
+    private final ArrayList<RegistryAction> mPreConnectedQueue = new ArrayList<>();
+    private volatile long mLastBwRefreshTime = 0;
+    private static final long BW_REFRESH_MIN_WIN_MS = 500;
+    private boolean mBandwidthUpdateScheduled = false;
+    private AtomicBoolean mBandwidthUpdatePending = new AtomicBoolean(false);
+    @NonNull
+    private NetworkInfo mNetworkInfo;
+    @NonNull
+    private final Object mRegisterLock = new Object();
+
+    /**
+     * The ID of the {@link NetworkProvider} that created this object, or
+     * {@link NetworkProvider#ID_NONE} if unknown.
+     * @hide
+     */
+    public final int providerId;
+
+    // ConnectivityService parses message constants from itself and NetworkAgent with MessageUtils
+    // for debugging purposes, and crashes if some messages have the same values.
+    // TODO: have ConnectivityService store message names in different maps and remove this base
+    private static final int BASE = 200;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to inform it of
+     * suspected connectivity problems on its network.  The NetworkAgent
+     * should take steps to verify and correct connectivity.
+     * @hide
+     */
+    public static final int CMD_SUSPECT_BAD = BASE;
+
+    /**
+     * Sent by the NetworkAgent (note the EVENT vs CMD prefix) to
+     * ConnectivityService to pass the current NetworkInfo (connection state).
+     * Sent when the NetworkInfo changes, mainly due to change of state.
+     * obj = NetworkInfo
+     * @hide
+     */
+    public static final int EVENT_NETWORK_INFO_CHANGED = BASE + 1;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the current
+     * NetworkCapabilties.
+     * obj = NetworkCapabilities
+     * @hide
+     */
+    public static final int EVENT_NETWORK_CAPABILITIES_CHANGED = BASE + 2;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the current
+     * NetworkProperties.
+     * obj = NetworkProperties
+     * @hide
+     */
+    public static final int EVENT_NETWORK_PROPERTIES_CHANGED = BASE + 3;
+
+    /**
+     * Centralize the place where base network score, and network score scaling, will be
+     * stored, so as we can consistently compare apple and oranges, or wifi, ethernet and LTE
+     * @hide
+     */
+    public static final int WIFI_BASE_SCORE = 60;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the current
+     * network score.
+     * arg1 = network score int
+     * @hide
+     */
+    public static final int EVENT_NETWORK_SCORE_CHANGED = BASE + 4;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the current
+     * list of underlying networks.
+     * obj = array of Network objects
+     * @hide
+     */
+    public static final int EVENT_UNDERLYING_NETWORKS_CHANGED = BASE + 5;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the current value of the teardown
+     * delay.
+     * arg1 = teardown delay in milliseconds
+     * @hide
+     */
+    public static final int EVENT_TEARDOWN_DELAY_CHANGED = BASE + 6;
+
+    /**
+     * The maximum value for the teardown delay, in milliseconds.
+     * @hide
+     */
+    public static final int MAX_TEARDOWN_DELAY_MS = 5000;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to inform the agent of the
+     * networks status - whether we could use the network or could not, due to
+     * either a bad network configuration (no internet link) or captive portal.
+     *
+     * arg1 = either {@code VALID_NETWORK} or {@code INVALID_NETWORK}
+     * obj = Bundle containing map from {@code REDIRECT_URL_KEY} to {@code String}
+     *       representing URL that Internet probe was redirect to, if it was redirected,
+     *       or mapping to {@code null} otherwise.
+     * @hide
+     */
+    public static final int CMD_REPORT_NETWORK_STATUS = BASE + 7;
+
+    /**
+     * Network validation suceeded.
+     * Corresponds to {@link NetworkCapabilities.NET_CAPABILITY_VALIDATED}.
+     */
+    public static final int VALIDATION_STATUS_VALID = 1;
+
+    /**
+     * Network validation was attempted and failed. This may be received more than once as
+     * subsequent validation attempts are made.
+     */
+    public static final int VALIDATION_STATUS_NOT_VALID = 2;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "VALIDATION_STATUS_" }, value = {
+            VALIDATION_STATUS_VALID,
+            VALIDATION_STATUS_NOT_VALID
+    })
+    public @interface ValidationStatus {}
+
+    // TODO: remove.
+    /** @hide */
+    public static final int VALID_NETWORK = 1;
+    /** @hide */
+    public static final int INVALID_NETWORK = 2;
+
+    /**
+     * The key for the redirect URL in the Bundle argument of {@code CMD_REPORT_NETWORK_STATUS}.
+     * @hide
+     */
+    public static final String REDIRECT_URL_KEY = "redirect URL";
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to indicate this network was
+     * explicitly selected.  This should be sent before the NetworkInfo is marked
+     * CONNECTED so it can be given special treatment at that time.
+     *
+     * obj = boolean indicating whether to use this network even if unvalidated
+     * @hide
+     */
+    public static final int EVENT_SET_EXPLICITLY_SELECTED = BASE + 8;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to inform the agent of
+     * whether the network should in the future be used even if not validated.
+     * This decision is made by the user, but it is the network transport's
+     * responsibility to remember it.
+     *
+     * arg1 = 1 if true, 0 if false
+     * @hide
+     */
+    public static final int CMD_SAVE_ACCEPT_UNVALIDATED = BASE + 9;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to inform the agent to pull
+     * the underlying network connection for updated bandwidth information.
+     * @hide
+     */
+    public static final int CMD_REQUEST_BANDWIDTH_UPDATE = BASE + 10;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to request that the specified packet be sent
+     * periodically on the given interval.
+     *
+     *   arg1 = the hardware slot number of the keepalive to start
+     *   arg2 = interval in seconds
+     *   obj = KeepalivePacketData object describing the data to be sent
+     *
+     * Also used internally by ConnectivityService / KeepaliveTracker, with different semantics.
+     * @hide
+     */
+    public static final int CMD_START_SOCKET_KEEPALIVE = BASE + 11;
+
+    /**
+     * Requests that the specified keepalive packet be stopped.
+     *
+     * arg1 = hardware slot number of the keepalive to stop.
+     *
+     * Also used internally by ConnectivityService / KeepaliveTracker, with different semantics.
+     * @hide
+     */
+    public static final int CMD_STOP_SOCKET_KEEPALIVE = BASE + 12;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to provide status on a socket keepalive
+     * request. This may either be the reply to a CMD_START_SOCKET_KEEPALIVE, or an asynchronous
+     * error notification.
+     *
+     * This is also sent by KeepaliveTracker to the app's {@link SocketKeepalive},
+     * so that the app's {@link SocketKeepalive.Callback} methods can be called.
+     *
+     * arg1 = hardware slot number of the keepalive
+     * arg2 = error code
+     * @hide
+     */
+    public static final int EVENT_SOCKET_KEEPALIVE = BASE + 13;
+
+    /**
+     * Sent by ConnectivityService to inform this network transport of signal strength thresholds
+     * that when crossed should trigger a system wakeup and a NetworkCapabilities update.
+     *
+     *   obj = int[] describing signal strength thresholds.
+     * @hide
+     */
+    public static final int CMD_SET_SIGNAL_STRENGTH_THRESHOLDS = BASE + 14;
+
+    /**
+     * Sent by ConnectivityService to the NeworkAgent to inform the agent to avoid
+     * automatically reconnecting to this network (e.g. via autojoin).  Happens
+     * when user selects "No" option on the "Stay connected?" dialog box.
+     * @hide
+     */
+    public static final int CMD_PREVENT_AUTOMATIC_RECONNECT = BASE + 15;
+
+    /**
+     * Sent by the KeepaliveTracker to NetworkAgent to add a packet filter.
+     *
+     * For TCP keepalive offloads, keepalive packets are sent by the firmware. However, because the
+     * remote site will send ACK packets in response to the keepalive packets, the firmware also
+     * needs to be configured to properly filter the ACKs to prevent the system from waking up.
+     * This does not happen with UDP, so this message is TCP-specific.
+     * arg1 = hardware slot number of the keepalive to filter for.
+     * obj = the keepalive packet to send repeatedly.
+     * @hide
+     */
+    public static final int CMD_ADD_KEEPALIVE_PACKET_FILTER = BASE + 16;
+
+    /**
+     * Sent by the KeepaliveTracker to NetworkAgent to remove a packet filter. See
+     * {@link #CMD_ADD_KEEPALIVE_PACKET_FILTER}.
+     * arg1 = hardware slot number of the keepalive packet filter to remove.
+     * @hide
+     */
+    public static final int CMD_REMOVE_KEEPALIVE_PACKET_FILTER = BASE + 17;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to complete the bidirectional connection.
+     * obj = INetworkAgentRegistry
+     */
+    private static final int EVENT_AGENT_CONNECTED = BASE + 18;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to inform the agent that it was disconnected.
+     */
+    private static final int EVENT_AGENT_DISCONNECTED = BASE + 19;
+
+    /**
+     * Sent by QosCallbackTracker to {@link NetworkAgent} to register a new filter with
+     * callback.
+     *
+     * arg1 = QoS agent callback ID
+     * obj = {@link QosFilter}
+     * @hide
+     */
+    public static final int CMD_REGISTER_QOS_CALLBACK = BASE + 20;
+
+    /**
+     * Sent by QosCallbackTracker to {@link NetworkAgent} to unregister a callback.
+     *
+     * arg1 = QoS agent callback ID
+     * @hide
+     */
+    public static final int CMD_UNREGISTER_QOS_CALLBACK = BASE + 21;
+
+    /**
+     * Sent by ConnectivityService to {@link NetworkAgent} to inform the agent that its native
+     * network was created and the Network object is now valid.
+     *
+     * @hide
+     */
+    public static final int CMD_NETWORK_CREATED = BASE + 22;
+
+    /**
+     * Sent by ConnectivityService to {@link NetworkAgent} to inform the agent that its native
+     * network was destroyed.
+     *
+     * @hide
+     */
+    public static final int CMD_NETWORK_DESTROYED = BASE + 23;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to set the linger duration for this network
+     * agent.
+     * arg1 = the linger duration, represents by {@link Duration}.
+     *
+     * @hide
+     */
+    public static final int EVENT_LINGER_DURATION_CHANGED = BASE + 24;
+
+    private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) {
+        final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType,
+                config.legacyTypeName, config.legacySubTypeName);
+        ni.setIsAvailable(true);
+        ni.setDetailedState(NetworkInfo.DetailedState.CONNECTING, null /* reason */,
+                config.getLegacyExtraInfo());
+        return ni;
+    }
+
+    // Temporary backward compatibility constructor
+    public NetworkAgent(@NonNull Context context, @NonNull Looper looper, @NonNull String logTag,
+            @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp, int score,
+            @NonNull NetworkAgentConfig config, @Nullable NetworkProvider provider) {
+        this(context, looper, logTag, nc, lp,
+                new NetworkScore.Builder().setLegacyInt(score).build(), config, provider);
+    }
+
+    /**
+     * Create a new network agent.
+     * @param context a {@link Context} to get system services from.
+     * @param looper the {@link Looper} on which to invoke the callbacks.
+     * @param logTag the tag for logs
+     * @param nc the initial {@link NetworkCapabilities} of this network. Update with
+     *           sendNetworkCapabilities.
+     * @param lp the initial {@link LinkProperties} of this network. Update with sendLinkProperties.
+     * @param score the initial score of this network. Update with sendNetworkScore.
+     * @param config an immutable {@link NetworkAgentConfig} for this agent.
+     * @param provider the {@link NetworkProvider} managing this agent.
+     */
+    public NetworkAgent(@NonNull Context context, @NonNull Looper looper, @NonNull String logTag,
+            @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
+            @NonNull NetworkScore score, @NonNull NetworkAgentConfig config,
+            @Nullable NetworkProvider provider) {
+        this(looper, context, logTag, nc, lp, score, config,
+                provider == null ? NetworkProvider.ID_NONE : provider.getProviderId(),
+                getLegacyNetworkInfo(config));
+    }
+
+    private static class InitialConfiguration {
+        public final Context context;
+        public final NetworkCapabilities capabilities;
+        public final LinkProperties properties;
+        public final NetworkScore score;
+        public final NetworkAgentConfig config;
+        public final NetworkInfo info;
+        InitialConfiguration(@NonNull Context context, @NonNull NetworkCapabilities capabilities,
+                @NonNull LinkProperties properties, @NonNull NetworkScore score,
+                @NonNull NetworkAgentConfig config, @NonNull NetworkInfo info) {
+            this.context = context;
+            this.capabilities = capabilities;
+            this.properties = properties;
+            this.score = score;
+            this.config = config;
+            this.info = info;
+        }
+    }
+    private volatile InitialConfiguration mInitialConfiguration;
+
+    private NetworkAgent(@NonNull Looper looper, @NonNull Context context, @NonNull String logTag,
+            @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
+            @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, int providerId,
+            @NonNull NetworkInfo ni) {
+        mHandler = new NetworkAgentHandler(looper);
+        LOG_TAG = logTag;
+        mNetworkInfo = new NetworkInfo(ni);
+        this.providerId = providerId;
+        if (ni == null || nc == null || lp == null) {
+            throw new IllegalArgumentException();
+        }
+
+        mInitialConfiguration = new InitialConfiguration(context,
+                new NetworkCapabilities(nc, NetworkCapabilities.REDACT_NONE),
+                new LinkProperties(lp), score, config, ni);
+    }
+
+    private class NetworkAgentHandler extends Handler {
+        NetworkAgentHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_AGENT_CONNECTED: {
+                    if (mRegistry != null) {
+                        log("Received new connection while already connected!");
+                    } else {
+                        if (VDBG) log("NetworkAgent fully connected");
+                        synchronized (mPreConnectedQueue) {
+                            final INetworkAgentRegistry registry = (INetworkAgentRegistry) msg.obj;
+                            mRegistry = registry;
+                            for (RegistryAction a : mPreConnectedQueue) {
+                                try {
+                                    a.execute(registry);
+                                } catch (RemoteException e) {
+                                    Log.wtf(LOG_TAG, "Communication error with registry", e);
+                                    // Fall through
+                                }
+                            }
+                            mPreConnectedQueue.clear();
+                        }
+                    }
+                    break;
+                }
+                case EVENT_AGENT_DISCONNECTED: {
+                    if (DBG) log("NetworkAgent channel lost");
+                    // let the client know CS is done with us.
+                    onNetworkUnwanted();
+                    synchronized (mPreConnectedQueue) {
+                        mRegistry = null;
+                    }
+                    break;
+                }
+                case CMD_SUSPECT_BAD: {
+                    log("Unhandled Message " + msg);
+                    break;
+                }
+                case CMD_REQUEST_BANDWIDTH_UPDATE: {
+                    long currentTimeMs = System.currentTimeMillis();
+                    if (VDBG) {
+                        log("CMD_REQUEST_BANDWIDTH_UPDATE request received.");
+                    }
+                    if (currentTimeMs >= (mLastBwRefreshTime + BW_REFRESH_MIN_WIN_MS)) {
+                        mBandwidthUpdateScheduled = false;
+                        if (!mBandwidthUpdatePending.getAndSet(true)) {
+                            onBandwidthUpdateRequested();
+                        }
+                    } else {
+                        // deliver the request at a later time rather than discard it completely.
+                        if (!mBandwidthUpdateScheduled) {
+                            long waitTime = mLastBwRefreshTime + BW_REFRESH_MIN_WIN_MS
+                                    - currentTimeMs + 1;
+                            mBandwidthUpdateScheduled = sendEmptyMessageDelayed(
+                                    CMD_REQUEST_BANDWIDTH_UPDATE, waitTime);
+                        }
+                    }
+                    break;
+                }
+                case CMD_REPORT_NETWORK_STATUS: {
+                    String redirectUrl = ((Bundle) msg.obj).getString(REDIRECT_URL_KEY);
+                    if (VDBG) {
+                        log("CMD_REPORT_NETWORK_STATUS("
+                                + (msg.arg1 == VALID_NETWORK ? "VALID, " : "INVALID, ")
+                                + redirectUrl);
+                    }
+                    Uri uri = null;
+                    try {
+                        if (null != redirectUrl) {
+                            uri = Uri.parse(redirectUrl);
+                        }
+                    } catch (Exception e) {
+                        Log.wtf(LOG_TAG, "Surprising URI : " + redirectUrl, e);
+                    }
+                    onValidationStatus(msg.arg1 /* status */, uri);
+                    break;
+                }
+                case CMD_SAVE_ACCEPT_UNVALIDATED: {
+                    onSaveAcceptUnvalidated(msg.arg1 != 0);
+                    break;
+                }
+                case CMD_START_SOCKET_KEEPALIVE: {
+                    onStartSocketKeepalive(msg.arg1 /* slot */,
+                            Duration.ofSeconds(msg.arg2) /* interval */,
+                            (KeepalivePacketData) msg.obj /* packet */);
+                    break;
+                }
+                case CMD_STOP_SOCKET_KEEPALIVE: {
+                    onStopSocketKeepalive(msg.arg1 /* slot */);
+                    break;
+                }
+
+                case CMD_SET_SIGNAL_STRENGTH_THRESHOLDS: {
+                    onSignalStrengthThresholdsUpdated((int[]) msg.obj);
+                    break;
+                }
+                case CMD_PREVENT_AUTOMATIC_RECONNECT: {
+                    onAutomaticReconnectDisabled();
+                    break;
+                }
+                case CMD_ADD_KEEPALIVE_PACKET_FILTER: {
+                    onAddKeepalivePacketFilter(msg.arg1 /* slot */,
+                            (KeepalivePacketData) msg.obj /* packet */);
+                    break;
+                }
+                case CMD_REMOVE_KEEPALIVE_PACKET_FILTER: {
+                    onRemoveKeepalivePacketFilter(msg.arg1 /* slot */);
+                    break;
+                }
+                case CMD_REGISTER_QOS_CALLBACK: {
+                    onQosCallbackRegistered(
+                            msg.arg1 /* QoS callback id */,
+                            (QosFilter) msg.obj /* QoS filter */);
+                    break;
+                }
+                case CMD_UNREGISTER_QOS_CALLBACK: {
+                    onQosCallbackUnregistered(
+                            msg.arg1 /* QoS callback id */);
+                    break;
+                }
+                case CMD_NETWORK_CREATED: {
+                    onNetworkCreated();
+                    break;
+                }
+                case CMD_NETWORK_DESTROYED: {
+                    onNetworkDestroyed();
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Register this network agent with ConnectivityService.
+     *
+     * This method can only be called once per network agent.
+     *
+     * @return the Network associated with this network agent (which can also be obtained later
+     *         by calling getNetwork() on this agent).
+     * @throws IllegalStateException thrown by the system server if this network agent is
+     *         already registered.
+     */
+    @NonNull
+    public Network register() {
+        if (VDBG) log("Registering NetworkAgent");
+        synchronized (mRegisterLock) {
+            if (mNetwork != null) {
+                throw new IllegalStateException("Agent already registered");
+            }
+            final ConnectivityManager cm = (ConnectivityManager) mInitialConfiguration.context
+                    .getSystemService(Context.CONNECTIVITY_SERVICE);
+            mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
+                    new NetworkInfo(mInitialConfiguration.info),
+                    mInitialConfiguration.properties, mInitialConfiguration.capabilities,
+                    mInitialConfiguration.score, mInitialConfiguration.config, providerId);
+            mInitialConfiguration = null; // All this memory can now be GC'd
+        }
+        return mNetwork;
+    }
+
+    private static class NetworkAgentBinder extends INetworkAgent.Stub {
+        private static final String LOG_TAG = NetworkAgentBinder.class.getSimpleName();
+
+        private final Handler mHandler;
+
+        private NetworkAgentBinder(Handler handler) {
+            mHandler = handler;
+        }
+
+        @Override
+        public void onRegistered(@NonNull INetworkAgentRegistry registry) {
+            mHandler.sendMessage(mHandler.obtainMessage(EVENT_AGENT_CONNECTED, registry));
+        }
+
+        @Override
+        public void onDisconnected() {
+            mHandler.sendMessage(mHandler.obtainMessage(EVENT_AGENT_DISCONNECTED));
+        }
+
+        @Override
+        public void onBandwidthUpdateRequested() {
+            mHandler.sendMessage(mHandler.obtainMessage(CMD_REQUEST_BANDWIDTH_UPDATE));
+        }
+
+        @Override
+        public void onValidationStatusChanged(
+                int validationStatus, @Nullable String captivePortalUrl) {
+            // TODO: consider using a parcelable as argument when the interface is structured
+            Bundle redirectUrlBundle = new Bundle();
+            redirectUrlBundle.putString(NetworkAgent.REDIRECT_URL_KEY, captivePortalUrl);
+            mHandler.sendMessage(mHandler.obtainMessage(CMD_REPORT_NETWORK_STATUS,
+                    validationStatus, 0, redirectUrlBundle));
+        }
+
+        @Override
+        public void onSaveAcceptUnvalidated(boolean acceptUnvalidated) {
+            mHandler.sendMessage(mHandler.obtainMessage(CMD_SAVE_ACCEPT_UNVALIDATED,
+                    acceptUnvalidated ? 1 : 0, 0));
+        }
+
+        @Override
+        public void onStartNattSocketKeepalive(int slot, int intervalDurationMs,
+                @NonNull NattKeepalivePacketData packetData) {
+            mHandler.sendMessage(mHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE,
+                    slot, intervalDurationMs, packetData));
+        }
+
+        @Override
+        public void onStartTcpSocketKeepalive(int slot, int intervalDurationMs,
+                @NonNull TcpKeepalivePacketData packetData) {
+            mHandler.sendMessage(mHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE,
+                    slot, intervalDurationMs, packetData));
+        }
+
+        @Override
+        public void onStopSocketKeepalive(int slot) {
+            mHandler.sendMessage(mHandler.obtainMessage(CMD_STOP_SOCKET_KEEPALIVE, slot, 0));
+        }
+
+        @Override
+        public void onSignalStrengthThresholdsUpdated(@NonNull int[] thresholds) {
+            mHandler.sendMessage(mHandler.obtainMessage(
+                    CMD_SET_SIGNAL_STRENGTH_THRESHOLDS, thresholds));
+        }
+
+        @Override
+        public void onPreventAutomaticReconnect() {
+            mHandler.sendMessage(mHandler.obtainMessage(CMD_PREVENT_AUTOMATIC_RECONNECT));
+        }
+
+        @Override
+        public void onAddNattKeepalivePacketFilter(int slot,
+                @NonNull NattKeepalivePacketData packetData) {
+            mHandler.sendMessage(mHandler.obtainMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER,
+                    slot, 0, packetData));
+        }
+
+        @Override
+        public void onAddTcpKeepalivePacketFilter(int slot,
+                @NonNull TcpKeepalivePacketData packetData) {
+            mHandler.sendMessage(mHandler.obtainMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER,
+                    slot, 0, packetData));
+        }
+
+        @Override
+        public void onRemoveKeepalivePacketFilter(int slot) {
+            mHandler.sendMessage(mHandler.obtainMessage(CMD_REMOVE_KEEPALIVE_PACKET_FILTER,
+                    slot, 0));
+        }
+
+        @Override
+        public void onQosFilterCallbackRegistered(final int qosCallbackId,
+                final QosFilterParcelable qosFilterParcelable) {
+            if (qosFilterParcelable.getQosFilter() != null) {
+                mHandler.sendMessage(
+                        mHandler.obtainMessage(CMD_REGISTER_QOS_CALLBACK, qosCallbackId, 0,
+                                qosFilterParcelable.getQosFilter()));
+                return;
+            }
+
+            Log.wtf(LOG_TAG, "onQosFilterCallbackRegistered: qos filter is null.");
+        }
+
+        @Override
+        public void onQosCallbackUnregistered(final int qosCallbackId) {
+            mHandler.sendMessage(mHandler.obtainMessage(
+                    CMD_UNREGISTER_QOS_CALLBACK, qosCallbackId, 0, null));
+        }
+
+        @Override
+        public void onNetworkCreated() {
+            mHandler.sendMessage(mHandler.obtainMessage(CMD_NETWORK_CREATED));
+        }
+
+        @Override
+        public void onNetworkDestroyed() {
+            mHandler.sendMessage(mHandler.obtainMessage(CMD_NETWORK_DESTROYED));
+        }
+    }
+
+    /**
+     * Register this network agent with a testing harness.
+     *
+     * The returned Messenger sends messages to the Handler. This allows a test to send
+     * this object {@code CMD_*} messages as if they came from ConnectivityService, which
+     * is useful for testing the behavior.
+     *
+     * @hide
+     */
+    public INetworkAgent registerForTest(final Network network) {
+        log("Registering NetworkAgent for test");
+        synchronized (mRegisterLock) {
+            mNetwork = network;
+            mInitialConfiguration = null;
+        }
+        return new NetworkAgentBinder(mHandler);
+    }
+
+    /**
+     * Waits for the handler to be idle.
+     * This is useful for testing, and has smaller scope than an accessor to mHandler.
+     * TODO : move the implementation in common library with the tests
+     * @hide
+     */
+    @VisibleForTesting
+    public boolean waitForIdle(final long timeoutMs) {
+        final ConditionVariable cv = new ConditionVariable(false);
+        mHandler.post(cv::open);
+        return cv.block(timeoutMs);
+    }
+
+    /**
+     * @return The Network associated with this agent, or null if it's not registered yet.
+     */
+    @Nullable
+    public Network getNetwork() {
+        return mNetwork;
+    }
+
+    private void queueOrSendMessage(@NonNull RegistryAction action) {
+        synchronized (mPreConnectedQueue) {
+            if (mRegistry != null) {
+                try {
+                    action.execute(mRegistry);
+                } catch (RemoteException e) {
+                    Log.wtf(LOG_TAG, "Error executing registry action", e);
+                    // Fall through: the channel is asynchronous and does not report errors back
+                }
+            } else {
+                mPreConnectedQueue.add(action);
+            }
+        }
+    }
+
+    /**
+     * Must be called by the agent when the network's {@link LinkProperties} change.
+     * @param linkProperties the new LinkProperties.
+     */
+    public final void sendLinkProperties(@NonNull LinkProperties linkProperties) {
+        Objects.requireNonNull(linkProperties);
+        final LinkProperties lp = new LinkProperties(linkProperties);
+        queueOrSendMessage(reg -> reg.sendLinkProperties(lp));
+    }
+
+    /**
+     * Must be called by the agent when the network's underlying networks change.
+     *
+     * <p>{@code networks} is one of the following:
+     * <ul>
+     * <li><strong>a non-empty array</strong>: an array of one or more {@link Network}s, in
+     * decreasing preference order. For example, if this VPN uses both wifi and mobile (cellular)
+     * networks to carry app traffic, but prefers or uses wifi more than mobile, wifi should appear
+     * first in the array.</li>
+     * <li><strong>an empty array</strong>: a zero-element array, meaning that the VPN has no
+     * underlying network connection, and thus, app traffic will not be sent or received.</li>
+     * <li><strong>null</strong>: (default) signifies that the VPN uses whatever is the system's
+     * default network. I.e., it doesn't use the {@code bindSocket} or {@code bindDatagramSocket}
+     * APIs mentioned above to send traffic over specific channels.</li>
+     * </ul>
+     *
+     * @param underlyingNetworks the new list of underlying networks.
+     * @see {@link VpnService.Builder#setUnderlyingNetworks(Network[])}
+     */
+    public final void setUnderlyingNetworks(
+            @SuppressLint("NullableCollection") @Nullable List<Network> underlyingNetworks) {
+        final ArrayList<Network> underlyingArray = (underlyingNetworks != null)
+                ? new ArrayList<>(underlyingNetworks) : null;
+        queueOrSendMessage(reg -> reg.sendUnderlyingNetworks(underlyingArray));
+    }
+
+    /**
+     * Inform ConnectivityService that this agent has now connected.
+     * Call {@link #unregister} to disconnect.
+     */
+    public void markConnected() {
+        mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTED, null /* reason */,
+                mNetworkInfo.getExtraInfo());
+        queueOrSendNetworkInfo(mNetworkInfo);
+    }
+
+    /**
+     * Unregister this network agent.
+     *
+     * This signals the network has disconnected and ends its lifecycle. After this is called,
+     * the network is torn down and this agent can no longer be used.
+     */
+    public void unregister() {
+        // When unregistering an agent nobody should use the extrainfo (or reason) any more.
+        mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED, null /* reason */,
+                null /* extraInfo */);
+        queueOrSendNetworkInfo(mNetworkInfo);
+    }
+
+    /**
+     * Sets the value of the teardown delay.
+     *
+     * The teardown delay is the time between when the network disconnects and when the native
+     * network corresponding to this {@code NetworkAgent} is destroyed. By default, the native
+     * network is destroyed immediately. If {@code teardownDelayMs} is non-zero, then when this
+     * network disconnects, the system will instead immediately mark the network as restricted
+     * and unavailable to unprivileged apps, but will defer destroying the native network until the
+     * teardown delay timer expires.
+     *
+     * The interfaces in use by this network will remain in use until the native network is
+     * destroyed and cannot be reused until {@link #onNetworkDestroyed()} is called.
+     *
+     * This method may be called at any time while the network is connected. It has no effect if
+     * the network is already disconnected and the teardown delay timer is running.
+     *
+     * @param teardownDelayMillis the teardown delay to set, or 0 to disable teardown delay.
+     */
+    public void setTeardownDelayMillis(
+            @IntRange(from = 0, to = MAX_TEARDOWN_DELAY_MS) int teardownDelayMillis) {
+        queueOrSendMessage(reg -> reg.sendTeardownDelayMs(teardownDelayMillis));
+    }
+
+    /**
+     * Change the legacy subtype of this network agent.
+     *
+     * This is only for backward compatibility and should not be used by non-legacy network agents,
+     * or agents that did not use to set a subtype. As such, only TYPE_MOBILE type agents can use
+     * this and others will be thrown an exception if they try.
+     *
+     * @deprecated this is for backward compatibility only.
+     * @param legacySubtype the legacy subtype.
+     * @hide
+     */
+    @Deprecated
+    @SystemApi
+    public void setLegacySubtype(final int legacySubtype, @NonNull final String legacySubtypeName) {
+        mNetworkInfo.setSubtype(legacySubtype, legacySubtypeName);
+        queueOrSendNetworkInfo(mNetworkInfo);
+    }
+
+    /**
+     * Set the ExtraInfo of this network agent.
+     *
+     * This sets the ExtraInfo field inside the NetworkInfo returned by legacy public API and the
+     * broadcasts about the corresponding Network.
+     * This is only for backward compatibility and should not be used by non-legacy network agents,
+     * who will be thrown an exception if they try. The extra info should only be :
+     * <ul>
+     *   <li>For cellular agents, the APN name.</li>
+     *   <li>For ethernet agents, the interface name.</li>
+     * </ul>
+     *
+     * @deprecated this is for backward compatibility only.
+     * @param extraInfo the ExtraInfo.
+     * @hide
+     */
+    @Deprecated
+    public void setLegacyExtraInfo(@Nullable final String extraInfo) {
+        mNetworkInfo.setExtraInfo(extraInfo);
+        queueOrSendNetworkInfo(mNetworkInfo);
+    }
+
+    /**
+     * Must be called by the agent when it has a new NetworkInfo object.
+     * @hide TODO: expose something better.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public final void sendNetworkInfo(NetworkInfo networkInfo) {
+        queueOrSendNetworkInfo(new NetworkInfo(networkInfo));
+    }
+
+    private void queueOrSendNetworkInfo(NetworkInfo networkInfo) {
+        queueOrSendMessage(reg -> reg.sendNetworkInfo(networkInfo));
+    }
+
+    /**
+     * Must be called by the agent when the network's {@link NetworkCapabilities} change.
+     * @param networkCapabilities the new NetworkCapabilities.
+     */
+    public final void sendNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
+        Objects.requireNonNull(networkCapabilities);
+        mBandwidthUpdatePending.set(false);
+        mLastBwRefreshTime = System.currentTimeMillis();
+        final NetworkCapabilities nc =
+                new NetworkCapabilities(networkCapabilities, NetworkCapabilities.REDACT_NONE);
+        queueOrSendMessage(reg -> reg.sendNetworkCapabilities(nc));
+    }
+
+    /**
+     * Must be called by the agent to update the score of this network.
+     *
+     * @param score the new score.
+     */
+    public final void sendNetworkScore(@NonNull NetworkScore score) {
+        Objects.requireNonNull(score);
+        queueOrSendMessage(reg -> reg.sendScore(score));
+    }
+
+    /**
+     * Must be called by the agent to update the score of this network.
+     *
+     * @param score the new score, between 0 and 99.
+     * deprecated use sendNetworkScore(NetworkScore) TODO : remove in S.
+     */
+    public final void sendNetworkScore(@IntRange(from = 0, to = 99) int score) {
+        sendNetworkScore(new NetworkScore.Builder().setLegacyInt(score).build());
+    }
+
+    /**
+     * Must be called by the agent to indicate this network was manually selected by the user.
+     * This should be called before the NetworkInfo is marked CONNECTED so that this
+     * Network can be given special treatment at that time. If {@code acceptUnvalidated} is
+     * {@code true}, then the system will switch to this network. If it is {@code false} and the
+     * network cannot be validated, the system will ask the user whether to switch to this network.
+     * If the user confirms and selects "don't ask again", then the system will call
+     * {@link #saveAcceptUnvalidated} to persist the user's choice. Thus, if the transport ever
+     * calls this method with {@code acceptUnvalidated} set to {@code false}, it must also implement
+     * {@link #saveAcceptUnvalidated} to respect the user's choice.
+     * @hide should move to NetworkAgentConfig.
+     */
+    public void explicitlySelected(boolean acceptUnvalidated) {
+        explicitlySelected(true /* explicitlySelected */, acceptUnvalidated);
+    }
+
+    /**
+     * Must be called by the agent to indicate whether the network was manually selected by the
+     * user. This should be called before the network becomes connected, so it can be given
+     * special treatment when it does.
+     *
+     * If {@code explicitlySelected} is {@code true}, and {@code acceptUnvalidated} is {@code true},
+     * then the system will switch to this network. If {@code explicitlySelected} is {@code true}
+     * and {@code acceptUnvalidated} is {@code false}, and the  network cannot be validated, the
+     * system will ask the user whether to switch to this network.  If the user confirms and selects
+     * "don't ask again", then the system will call {@link #saveAcceptUnvalidated} to persist the
+     * user's choice. Thus, if the transport ever calls this method with {@code explicitlySelected}
+     * set to {@code true} and {@code acceptUnvalidated} set to {@code false}, it must also
+     * implement {@link #saveAcceptUnvalidated} to respect the user's choice.
+     *
+     * If  {@code explicitlySelected} is {@code false} and {@code acceptUnvalidated} is
+     * {@code true}, the system will interpret this as the user having accepted partial connectivity
+     * on this network. Thus, the system will switch to the network and consider it validated even
+     * if it only provides partial connectivity, but the network is not otherwise treated specially.
+     * @hide should move to NetworkAgentConfig.
+     */
+    public void explicitlySelected(boolean explicitlySelected, boolean acceptUnvalidated) {
+        queueOrSendMessage(reg -> reg.sendExplicitlySelected(
+                explicitlySelected, acceptUnvalidated));
+    }
+
+    /**
+     * Called when ConnectivityService has indicated they no longer want this network.
+     * The parent factory should (previously) have received indication of the change
+     * as well, either canceling NetworkRequests or altering their score such that this
+     * network won't be immediately requested again.
+     */
+    public void onNetworkUnwanted() {
+        unwanted();
+    }
+    /** @hide TODO delete once subclasses have moved to onNetworkUnwanted. */
+    protected void unwanted() {
+    }
+
+    /**
+     * Called when ConnectivityService request a bandwidth update. The parent factory
+     * shall try to overwrite this method and produce a bandwidth update if capable.
+     * @hide
+     */
+    @SystemApi
+    public void onBandwidthUpdateRequested() {
+        pollLceData();
+    }
+    /** @hide TODO delete once subclasses have moved to onBandwidthUpdateRequested. */
+    protected void pollLceData() {
+    }
+
+    /**
+     * Called when the system determines the usefulness of this network.
+     *
+     * The system attempts to validate Internet connectivity on networks that provide the
+     * {@link NetworkCapabilities#NET_CAPABILITY_INTERNET} capability.
+     *
+     * Currently there are two possible values:
+     * {@code VALIDATION_STATUS_VALID} if Internet connectivity was validated,
+     * {@code VALIDATION_STATUS_NOT_VALID} if Internet connectivity was not validated.
+     *
+     * This is guaranteed to be called again when the network status changes, but the system
+     * may also call this multiple times even if the status does not change.
+     *
+     * @param status one of {@code VALIDATION_STATUS_VALID} or {@code VALIDATION_STATUS_NOT_VALID}.
+     * @param redirectUri If Internet connectivity is being redirected (e.g., on a captive portal),
+     *        this is the destination the probes are being redirected to, otherwise {@code null}.
+     */
+    public void onValidationStatus(@ValidationStatus int status, @Nullable Uri redirectUri) {
+        networkStatus(status, null == redirectUri ? "" : redirectUri.toString());
+    }
+    /** @hide TODO delete once subclasses have moved to onValidationStatus */
+    protected void networkStatus(int status, String redirectUrl) {
+    }
+
+
+    /**
+     * Called when the user asks to remember the choice to use this network even if unvalidated.
+     * The transport is responsible for remembering the choice, and the next time the user connects
+     * to the network, should explicitlySelected with {@code acceptUnvalidated} set to {@code true}.
+     * This method will only be called if {@link #explicitlySelected} was called with
+     * {@code acceptUnvalidated} set to {@code false}.
+     * @param accept whether the user wants to use the network even if unvalidated.
+     */
+    public void onSaveAcceptUnvalidated(boolean accept) {
+        saveAcceptUnvalidated(accept);
+    }
+    /** @hide TODO delete once subclasses have moved to onSaveAcceptUnvalidated */
+    protected void saveAcceptUnvalidated(boolean accept) {
+    }
+
+    /**
+     * Called when ConnectivityService has successfully created this NetworkAgent's native network.
+     */
+    public void onNetworkCreated() {}
+
+
+    /**
+     * Called when ConnectivityService has successfully destroy this NetworkAgent's native network.
+     */
+    public void onNetworkDestroyed() {}
+
+    /**
+     * Requests that the network hardware send the specified packet at the specified interval.
+     *
+     * @param slot the hardware slot on which to start the keepalive.
+     * @param interval the interval between packets, between 10 and 3600. Note that this API
+     *                 does not support sub-second precision and will round off the request.
+     * @param packet the packet to send.
+     */
+    // seconds is from SocketKeepalive.MIN_INTERVAL_SEC to MAX_INTERVAL_SEC, but these should
+    // not be exposed as constants because they may change in the future (API guideline 4.8)
+    // and should have getters if exposed at all. Getters can't be used in the annotation,
+    // so the values unfortunately need to be copied.
+    public void onStartSocketKeepalive(int slot, @NonNull Duration interval,
+            @NonNull KeepalivePacketData packet) {
+        final long intervalSeconds = interval.getSeconds();
+        if (intervalSeconds < SocketKeepalive.MIN_INTERVAL_SEC
+                || intervalSeconds > SocketKeepalive.MAX_INTERVAL_SEC) {
+            throw new IllegalArgumentException("Interval needs to be comprised between "
+                    + SocketKeepalive.MIN_INTERVAL_SEC + " and " + SocketKeepalive.MAX_INTERVAL_SEC
+                    + " but was " + intervalSeconds);
+        }
+        final Message msg = mHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, slot,
+                (int) intervalSeconds, packet);
+        startSocketKeepalive(msg);
+        msg.recycle();
+    }
+    /** @hide TODO delete once subclasses have moved to onStartSocketKeepalive */
+    protected void startSocketKeepalive(Message msg) {
+        onSocketKeepaliveEvent(msg.arg1, SocketKeepalive.ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * Requests that the network hardware stop a previously-started keepalive.
+     *
+     * @param slot the hardware slot on which to stop the keepalive.
+     */
+    public void onStopSocketKeepalive(int slot) {
+        Message msg = mHandler.obtainMessage(CMD_STOP_SOCKET_KEEPALIVE, slot, 0, null);
+        stopSocketKeepalive(msg);
+        msg.recycle();
+    }
+    /** @hide TODO delete once subclasses have moved to onStopSocketKeepalive */
+    protected void stopSocketKeepalive(Message msg) {
+        onSocketKeepaliveEvent(msg.arg1, SocketKeepalive.ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * Must be called by the agent when a socket keepalive event occurs.
+     *
+     * @param slot the hardware slot on which the event occurred.
+     * @param event the event that occurred, as one of the SocketKeepalive.ERROR_*
+     *              or SocketKeepalive.SUCCESS constants.
+     */
+    public final void sendSocketKeepaliveEvent(int slot,
+            @SocketKeepalive.KeepaliveEvent int event) {
+        queueOrSendMessage(reg -> reg.sendSocketKeepaliveEvent(slot, event));
+    }
+    /** @hide TODO delete once callers have moved to sendSocketKeepaliveEvent */
+    public void onSocketKeepaliveEvent(int slot, int reason) {
+        sendSocketKeepaliveEvent(slot, reason);
+    }
+
+    /**
+     * Called by ConnectivityService to add specific packet filter to network hardware to block
+     * replies (e.g., TCP ACKs) matching the sent keepalive packets. Implementations that support
+     * this feature must override this method.
+     *
+     * @param slot the hardware slot on which the keepalive should be sent.
+     * @param packet the packet that is being sent.
+     */
+    public void onAddKeepalivePacketFilter(int slot, @NonNull KeepalivePacketData packet) {
+        Message msg = mHandler.obtainMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER, slot, 0, packet);
+        addKeepalivePacketFilter(msg);
+        msg.recycle();
+    }
+    /** @hide TODO delete once subclasses have moved to onAddKeepalivePacketFilter */
+    protected void addKeepalivePacketFilter(Message msg) {
+    }
+
+    /**
+     * Called by ConnectivityService to remove a packet filter installed with
+     * {@link #addKeepalivePacketFilter(Message)}. Implementations that support this feature
+     * must override this method.
+     *
+     * @param slot the hardware slot on which the keepalive is being sent.
+     */
+    public void onRemoveKeepalivePacketFilter(int slot) {
+        Message msg = mHandler.obtainMessage(CMD_REMOVE_KEEPALIVE_PACKET_FILTER, slot, 0, null);
+        removeKeepalivePacketFilter(msg);
+        msg.recycle();
+    }
+    /** @hide TODO delete once subclasses have moved to onRemoveKeepalivePacketFilter */
+    protected void removeKeepalivePacketFilter(Message msg) {
+    }
+
+    /**
+     * Called by ConnectivityService to inform this network agent of signal strength thresholds
+     * that when crossed should trigger a system wakeup and a NetworkCapabilities update.
+     *
+     * When the system updates the list of thresholds that should wake up the CPU for a
+     * given agent it will call this method on the agent. The agent that implement this
+     * should implement it in hardware so as to ensure the CPU will be woken up on breach.
+     * Agents are expected to react to a breach by sending an updated NetworkCapabilities
+     * object with the appropriate signal strength to sendNetworkCapabilities.
+     *
+     * The specific units are bearer-dependent. See details on the units and requests in
+     * {@link NetworkCapabilities.Builder#setSignalStrength}.
+     *
+     * @param thresholds the array of thresholds that should trigger wakeups.
+     */
+    public void onSignalStrengthThresholdsUpdated(@NonNull int[] thresholds) {
+        setSignalStrengthThresholds(thresholds);
+    }
+    /** @hide TODO delete once subclasses have moved to onSetSignalStrengthThresholds */
+    protected void setSignalStrengthThresholds(int[] thresholds) {
+    }
+
+    /**
+     * Called when the user asks to not stay connected to this network because it was found to not
+     * provide Internet access.  Usually followed by call to {@code unwanted}.  The transport is
+     * responsible for making sure the device does not automatically reconnect to the same network
+     * after the {@code unwanted} call.
+     */
+    public void onAutomaticReconnectDisabled() {
+        preventAutomaticReconnect();
+    }
+    /** @hide TODO delete once subclasses have moved to onAutomaticReconnectDisabled */
+    protected void preventAutomaticReconnect() {
+    }
+
+    /**
+     * Called when a qos callback is registered with a filter.
+     * @param qosCallbackId the id for the callback registered
+     * @param filter the filter being registered
+     */
+    public void onQosCallbackRegistered(final int qosCallbackId, final @NonNull QosFilter filter) {
+    }
+
+    /**
+     * Called when a qos callback is registered with a filter.
+     * <p/>
+     * Any QoS events that are sent with the same callback id after this method is called
+     * are a no-op.
+     *
+     * @param qosCallbackId the id for the callback being unregistered
+     */
+    public void onQosCallbackUnregistered(final int qosCallbackId) {
+    }
+
+
+    /**
+     * Sends the attributes of Qos Session back to the Application
+     *
+     * @param qosCallbackId the callback id that the session belongs to
+     * @param sessionId the unique session id across all Qos Sessions
+     * @param attributes the attributes of the Qos Session
+     */
+    public final void sendQosSessionAvailable(final int qosCallbackId, final int sessionId,
+            @NonNull final QosSessionAttributes attributes) {
+        Objects.requireNonNull(attributes, "The attributes must be non-null");
+        if (attributes instanceof EpsBearerQosSessionAttributes) {
+            queueOrSendMessage(ra -> ra.sendEpsQosSessionAvailable(qosCallbackId,
+                    new QosSession(sessionId, QosSession.TYPE_EPS_BEARER),
+                    (EpsBearerQosSessionAttributes)attributes));
+        } else if (attributes instanceof NrQosSessionAttributes) {
+            queueOrSendMessage(ra -> ra.sendNrQosSessionAvailable(qosCallbackId,
+                    new QosSession(sessionId, QosSession.TYPE_NR_BEARER),
+                    (NrQosSessionAttributes)attributes));
+        }
+    }
+
+    /**
+     * Sends event that the Qos Session was lost.
+     *
+     * @param qosCallbackId the callback id that the session belongs to
+     * @param sessionId the unique session id across all Qos Sessions
+     * @param qosSessionType the session type {@code QosSesson#QosSessionType}
+     */
+    public final void sendQosSessionLost(final int qosCallbackId,
+            final int sessionId, final int qosSessionType) {
+        queueOrSendMessage(ra -> ra.sendQosSessionLost(qosCallbackId,
+                new QosSession(sessionId, qosSessionType)));
+    }
+
+    /**
+     * Sends the exception type back to the application.
+     *
+     * The NetworkAgent should not send anymore messages with this id.
+     *
+     * @param qosCallbackId the callback id this exception belongs to
+     * @param exceptionType the type of exception
+     */
+    public final void sendQosCallbackError(final int qosCallbackId,
+            @QosCallbackException.ExceptionType final int exceptionType) {
+        queueOrSendMessage(ra -> ra.sendQosCallbackError(qosCallbackId, exceptionType));
+    }
+
+    /**
+     * Set the linger duration for this network agent.
+     * @param duration the delay between the moment the network becomes unneeded and the
+     *                 moment the network is disconnected or moved into the background.
+     *                 Note that If this duration has greater than millisecond precision, then
+     *                 the internal implementation will drop any excess precision.
+     */
+    public void setLingerDuration(@NonNull final Duration duration) {
+        Objects.requireNonNull(duration);
+        final long durationMs = duration.toMillis();
+        if (durationMs < MIN_LINGER_TIMER_MS || durationMs > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException("Duration must be within ["
+                    + MIN_LINGER_TIMER_MS + "," + Integer.MAX_VALUE + "]ms");
+        }
+        queueOrSendMessage(ra -> ra.sendLingerDuration((int) durationMs));
+    }
+
+    /** @hide */
+    protected void log(final String s) {
+        Log.d(LOG_TAG, "NetworkAgent: " + s);
+    }
+}
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
new file mode 100644
index 0000000..ad8396b
--- /dev/null
+++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -0,0 +1,505 @@
+/*
+ * 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.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Allows a network transport to provide the system with policy and configuration information about
+ * a particular network when registering a {@link NetworkAgent}. This information cannot change once the agent is registered.
+ *
+ * @hide
+ */
+@SystemApi
+public final class NetworkAgentConfig implements Parcelable {
+
+    /**
+     * If the {@link Network} is a VPN, whether apps are allowed to bypass the
+     * VPN. This is set by a {@link VpnService} and used by
+     * {@link ConnectivityManager} when creating a VPN.
+     *
+     * @hide
+     */
+    public boolean allowBypass;
+
+    /**
+     * Set if the network was manually/explicitly connected to by the user either from settings
+     * or a 3rd party app.  For example, turning on cell data is not explicit but tapping on a wifi
+     * ap in the wifi settings to trigger a connection is explicit.  A 3rd party app asking to
+     * connect to a particular access point is also explicit, though this may change in the future
+     * as we want apps to use the multinetwork apis.
+     * TODO : this is a bad name, because it sounds like the user just tapped on the network.
+     * It's not necessarily the case ; auto-reconnection to WiFi has this true for example.
+     * @hide
+     */
+    public boolean explicitlySelected;
+
+    /**
+     * @return whether this network was explicitly selected by the user.
+     */
+    public boolean isExplicitlySelected() {
+        return explicitlySelected;
+    }
+
+    /**
+     * @return whether this VPN connection can be bypassed by the apps.
+     *
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    public boolean isBypassableVpn() {
+        return allowBypass;
+    }
+
+    /**
+     * Set if the user desires to use this network even if it is unvalidated. This field has meaning
+     * only if {@link explicitlySelected} is true. If it is, this field must also be set to the
+     * appropriate value based on previous user choice.
+     *
+     * TODO : rename this field to match its accessor
+     * @hide
+     */
+    public boolean acceptUnvalidated;
+
+    /**
+     * @return whether the system should accept this network even if it doesn't validate.
+     */
+    public boolean isUnvalidatedConnectivityAcceptable() {
+        return acceptUnvalidated;
+    }
+
+    /**
+     * Whether the user explicitly set that this network should be validated even if presence of
+     * only partial internet connectivity.
+     *
+     * TODO : rename this field to match its accessor
+     * @hide
+     */
+    public boolean acceptPartialConnectivity;
+
+    /**
+     * @return whether the system should validate this network even if it only offers partial
+     *     Internet connectivity.
+     */
+    public boolean isPartialConnectivityAcceptable() {
+        return acceptPartialConnectivity;
+    }
+
+    /**
+     * Set to avoid surfacing the "Sign in to network" notification.
+     * if carrier receivers/apps are registered to handle the carrier-specific provisioning
+     * procedure, a carrier specific provisioning notification will be placed.
+     * only one notification should be displayed. This field is set based on
+     * which notification should be used for provisioning.
+     *
+     * @hide
+     */
+    public boolean provisioningNotificationDisabled;
+
+    /**
+     *
+     * @return whether the sign in to network notification is enabled by this configuration.
+     * @hide
+     */
+    public boolean isProvisioningNotificationEnabled() {
+        return !provisioningNotificationDisabled;
+    }
+
+    /**
+     * For mobile networks, this is the subscriber ID (such as IMSI).
+     *
+     * @hide
+     */
+    public String subscriberId;
+
+    /**
+     * @return the subscriber ID, or null if none.
+     * @hide
+     */
+    @SystemApi(client = MODULE_LIBRARIES)
+    @Nullable
+    public String getSubscriberId() {
+        return subscriberId;
+    }
+
+    /**
+     * Set to skip 464xlat. This means the device will treat the network as IPv6-only and
+     * will not attempt to detect a NAT64 via RFC 7050 DNS lookups.
+     *
+     * @hide
+     */
+    public boolean skip464xlat;
+
+    /**
+     * @return whether NAT64 prefix detection is enabled.
+     * @hide
+     */
+    public boolean isNat64DetectionEnabled() {
+        return !skip464xlat;
+    }
+
+    /**
+     * The legacy type of this network agent, or TYPE_NONE if unset.
+     * @hide
+     */
+    public int legacyType = ConnectivityManager.TYPE_NONE;
+
+    /**
+     * @return the legacy type
+     */
+    @ConnectivityManager.LegacyNetworkType
+    public int getLegacyType() {
+        return legacyType;
+    }
+
+    /**
+     * The legacy Sub type of this network agent, or TYPE_NONE if unset.
+     * @hide
+     */
+    public int legacySubType = ConnectivityManager.TYPE_NONE;
+
+    /**
+     * Set to true if the PRIVATE_DNS_BROKEN notification has shown for this network.
+     * Reset this bit when private DNS mode is changed from strict mode to opportunistic/off mode.
+     *
+     * This is not parceled, because it would not make sense.
+     *
+     * @hide
+     */
+    public transient boolean hasShownBroken;
+
+    /**
+     * The name of the legacy network type. It's a free-form string used in logging.
+     * @hide
+     */
+    @NonNull
+    public String legacyTypeName = "";
+
+    /**
+     * @return the name of the legacy network type. It's a free-form string used in logging.
+     */
+    @NonNull
+    public String getLegacyTypeName() {
+        return legacyTypeName;
+    }
+
+    /**
+     * The name of the legacy Sub network type. It's a free-form string.
+     * @hide
+     */
+    @NonNull
+    public String legacySubTypeName = "";
+
+    /**
+     * The legacy extra info of the agent. The extra info should only be :
+     * <ul>
+     *   <li>For cellular agents, the APN name.</li>
+     *   <li>For ethernet agents, the interface name.</li>
+     * </ul>
+     * @hide
+     */
+    @NonNull
+    private String mLegacyExtraInfo = "";
+
+    /**
+     * The legacy extra info of the agent.
+     * @hide
+     */
+    @NonNull
+    public String getLegacyExtraInfo() {
+        return mLegacyExtraInfo;
+    }
+
+    /** @hide */
+    public NetworkAgentConfig() {
+    }
+
+    /** @hide */
+    public NetworkAgentConfig(@Nullable NetworkAgentConfig nac) {
+        if (nac != null) {
+            allowBypass = nac.allowBypass;
+            explicitlySelected = nac.explicitlySelected;
+            acceptUnvalidated = nac.acceptUnvalidated;
+            acceptPartialConnectivity = nac.acceptPartialConnectivity;
+            subscriberId = nac.subscriberId;
+            provisioningNotificationDisabled = nac.provisioningNotificationDisabled;
+            skip464xlat = nac.skip464xlat;
+            legacyType = nac.legacyType;
+            legacyTypeName = nac.legacyTypeName;
+            legacySubType = nac.legacySubType;
+            legacySubTypeName = nac.legacySubTypeName;
+            mLegacyExtraInfo = nac.mLegacyExtraInfo;
+        }
+    }
+
+    /**
+     * Builder class to facilitate constructing {@link NetworkAgentConfig} objects.
+     */
+    public static final class Builder {
+        private final NetworkAgentConfig mConfig = new NetworkAgentConfig();
+
+        /**
+         * Sets whether the network was explicitly selected by the user.
+         *
+         * @return this builder, to facilitate chaining.
+         */
+        @NonNull
+        public Builder setExplicitlySelected(final boolean explicitlySelected) {
+            mConfig.explicitlySelected = explicitlySelected;
+            return this;
+        }
+
+        /**
+         * Sets whether the system should validate this network even if it is found not to offer
+         * Internet connectivity.
+         *
+         * @return this builder, to facilitate chaining.
+         */
+        @NonNull
+        public Builder setUnvalidatedConnectivityAcceptable(
+                final boolean unvalidatedConnectivityAcceptable) {
+            mConfig.acceptUnvalidated = unvalidatedConnectivityAcceptable;
+            return this;
+        }
+
+        /**
+         * Sets whether the system should validate this network even if it is found to only offer
+         * partial Internet connectivity.
+         *
+         * @return this builder, to facilitate chaining.
+         */
+        @NonNull
+        public Builder setPartialConnectivityAcceptable(
+                final boolean partialConnectivityAcceptable) {
+            mConfig.acceptPartialConnectivity = partialConnectivityAcceptable;
+            return this;
+        }
+
+        /**
+         * Sets the subscriber ID for this network.
+         *
+         * @return this builder, to facilitate chaining.
+         * @hide
+         */
+        @NonNull
+        @SystemApi(client = MODULE_LIBRARIES)
+        public Builder setSubscriberId(@Nullable String subscriberId) {
+            mConfig.subscriberId = subscriberId;
+            return this;
+        }
+
+        /**
+         * Enables or disables active detection of NAT64 (e.g., via RFC 7050 DNS lookups). Used to
+         * save power and reduce idle traffic on networks that are known to be IPv6-only without a
+         * NAT64. By default, NAT64 detection is enabled.
+         *
+         * @return this builder, to facilitate chaining.
+         */
+        @NonNull
+        public Builder setNat64DetectionEnabled(boolean enabled) {
+            mConfig.skip464xlat = !enabled;
+            return this;
+        }
+
+        /**
+         * Enables or disables the "Sign in to network" notification. Used if the network transport
+         * will perform its own carrier-specific provisioning procedure. By default, the
+         * notification is enabled.
+         *
+         * @return this builder, to facilitate chaining.
+         */
+        @NonNull
+        public Builder setProvisioningNotificationEnabled(boolean enabled) {
+            mConfig.provisioningNotificationDisabled = !enabled;
+            return this;
+        }
+
+        /**
+         * Sets the legacy type for this network.
+         *
+         * @param legacyType the type
+         * @return this builder, to facilitate chaining.
+         */
+        @NonNull
+        public Builder setLegacyType(int legacyType) {
+            mConfig.legacyType = legacyType;
+            return this;
+        }
+
+        /**
+         * Sets the legacy sub-type for this network.
+         *
+         * @param legacySubType the type
+         * @return this builder, to facilitate chaining.
+         */
+        @NonNull
+        public Builder setLegacySubType(final int legacySubType) {
+            mConfig.legacySubType = legacySubType;
+            return this;
+        }
+
+        /**
+         * Sets the name of the legacy type of the agent. It's a free-form string used in logging.
+         * @param legacyTypeName the name
+         * @return this builder, to facilitate chaining.
+         */
+        @NonNull
+        public Builder setLegacyTypeName(@NonNull String legacyTypeName) {
+            mConfig.legacyTypeName = legacyTypeName;
+            return this;
+        }
+
+        /**
+         * Sets the name of the legacy Sub-type of the agent. It's a free-form string.
+         * @param legacySubTypeName the name
+         * @return this builder, to facilitate chaining.
+         */
+        @NonNull
+        public Builder setLegacySubTypeName(@NonNull String legacySubTypeName) {
+            mConfig.legacySubTypeName = legacySubTypeName;
+            return this;
+        }
+
+        /**
+         * Sets the legacy extra info of the agent.
+         * @param legacyExtraInfo the legacy extra info.
+         * @return this builder, to facilitate chaining.
+         */
+        @NonNull
+        public Builder setLegacyExtraInfo(@NonNull String legacyExtraInfo) {
+            mConfig.mLegacyExtraInfo = legacyExtraInfo;
+            return this;
+        }
+
+        /**
+         * Sets whether the apps can bypass the VPN connection.
+         *
+         * @return this builder, to facilitate chaining.
+         * @hide
+         */
+        @NonNull
+        @SystemApi(client = MODULE_LIBRARIES)
+        public Builder setBypassableVpn(boolean allowBypass) {
+            mConfig.allowBypass = allowBypass;
+            return this;
+        }
+
+        /**
+         * Returns the constructed {@link NetworkAgentConfig} object.
+         */
+        @NonNull
+        public NetworkAgentConfig build() {
+            return mConfig;
+        }
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        final NetworkAgentConfig that = (NetworkAgentConfig) o;
+        return allowBypass == that.allowBypass
+                && explicitlySelected == that.explicitlySelected
+                && acceptUnvalidated == that.acceptUnvalidated
+                && acceptPartialConnectivity == that.acceptPartialConnectivity
+                && provisioningNotificationDisabled == that.provisioningNotificationDisabled
+                && skip464xlat == that.skip464xlat
+                && legacyType == that.legacyType
+                && Objects.equals(subscriberId, that.subscriberId)
+                && Objects.equals(legacyTypeName, that.legacyTypeName)
+                && Objects.equals(mLegacyExtraInfo, that.mLegacyExtraInfo);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(allowBypass, explicitlySelected, acceptUnvalidated,
+                acceptPartialConnectivity, provisioningNotificationDisabled, subscriberId,
+                skip464xlat, legacyType, legacyTypeName, mLegacyExtraInfo);
+    }
+
+    @Override
+    public String toString() {
+        return "NetworkAgentConfig {"
+                + " allowBypass = " + allowBypass
+                + ", explicitlySelected = " + explicitlySelected
+                + ", acceptUnvalidated = " + acceptUnvalidated
+                + ", acceptPartialConnectivity = " + acceptPartialConnectivity
+                + ", provisioningNotificationDisabled = " + provisioningNotificationDisabled
+                + ", subscriberId = '" + subscriberId + '\''
+                + ", skip464xlat = " + skip464xlat
+                + ", legacyType = " + legacyType
+                + ", hasShownBroken = " + hasShownBroken
+                + ", legacyTypeName = '" + legacyTypeName + '\''
+                + ", legacyExtraInfo = '" + mLegacyExtraInfo + '\''
+                + "}";
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeInt(allowBypass ? 1 : 0);
+        out.writeInt(explicitlySelected ? 1 : 0);
+        out.writeInt(acceptUnvalidated ? 1 : 0);
+        out.writeInt(acceptPartialConnectivity ? 1 : 0);
+        out.writeString(subscriberId);
+        out.writeInt(provisioningNotificationDisabled ? 1 : 0);
+        out.writeInt(skip464xlat ? 1 : 0);
+        out.writeInt(legacyType);
+        out.writeString(legacyTypeName);
+        out.writeInt(legacySubType);
+        out.writeString(legacySubTypeName);
+        out.writeString(mLegacyExtraInfo);
+    }
+
+    public static final @NonNull Creator<NetworkAgentConfig> CREATOR =
+            new Creator<NetworkAgentConfig>() {
+        @Override
+        public NetworkAgentConfig createFromParcel(Parcel in) {
+            NetworkAgentConfig networkAgentConfig = new NetworkAgentConfig();
+            networkAgentConfig.allowBypass = in.readInt() != 0;
+            networkAgentConfig.explicitlySelected = in.readInt() != 0;
+            networkAgentConfig.acceptUnvalidated = in.readInt() != 0;
+            networkAgentConfig.acceptPartialConnectivity = in.readInt() != 0;
+            networkAgentConfig.subscriberId = in.readString();
+            networkAgentConfig.provisioningNotificationDisabled = in.readInt() != 0;
+            networkAgentConfig.skip464xlat = in.readInt() != 0;
+            networkAgentConfig.legacyType = in.readInt();
+            networkAgentConfig.legacyTypeName = in.readString();
+            networkAgentConfig.legacySubType = in.readInt();
+            networkAgentConfig.legacySubTypeName = in.readString();
+            networkAgentConfig.mLegacyExtraInfo = in.readString();
+            return networkAgentConfig;
+        }
+
+        @Override
+        public NetworkAgentConfig[] newArray(int size) {
+            return new NetworkAgentConfig[size];
+        }
+    };
+}
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
new file mode 100644
index 0000000..3a07db3
--- /dev/null
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -0,0 +1,2776 @@
+/*
+ * 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 com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
+import android.annotation.IntDef;
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.wifi.WifiNetworkSuggestion;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Range;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.NetworkCapabilitiesUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Set;
+import java.util.StringJoiner;
+
+/**
+ * Representation of the capabilities of an active network. Instances are
+ * typically obtained through
+ * {@link NetworkCallback#onCapabilitiesChanged(Network, NetworkCapabilities)}
+ * or {@link ConnectivityManager#getNetworkCapabilities(Network)}.
+ * <p>
+ * This replaces the old {@link ConnectivityManager#TYPE_MOBILE} method of
+ * network selection. Rather than indicate a need for Wi-Fi because an
+ * application needs high bandwidth and risk obsolescence when a new, fast
+ * network appears (like LTE), the application should specify it needs high
+ * bandwidth. Similarly if an application needs an unmetered network for a bulk
+ * transfer it can specify that rather than assuming all cellular based
+ * connections are metered and all Wi-Fi based connections are not.
+ */
+public final class NetworkCapabilities implements Parcelable {
+    private static final String TAG = "NetworkCapabilities";
+
+    /**
+     * Mechanism to support redaction of fields in NetworkCapabilities that are guarded by specific
+     * app permissions.
+     **/
+    /**
+     * Don't redact any fields since the receiving app holds all the necessary permissions.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final long REDACT_NONE = 0;
+
+    /**
+     * Redact any fields that need {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
+     * permission since the receiving app does not hold this permission or the location toggle
+     * is off.
+     *
+     * @see android.Manifest.permission#ACCESS_FINE_LOCATION
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final long REDACT_FOR_ACCESS_FINE_LOCATION = 1 << 0;
+
+    /**
+     * Redact any fields that need {@link android.Manifest.permission#LOCAL_MAC_ADDRESS}
+     * permission since the receiving app does not hold this permission.
+     *
+     * @see android.Manifest.permission#LOCAL_MAC_ADDRESS
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final long REDACT_FOR_LOCAL_MAC_ADDRESS = 1 << 1;
+
+    /**
+     *
+     * Redact any fields that need {@link android.Manifest.permission#NETWORK_SETTINGS}
+     * permission since the receiving app does not hold this permission.
+     *
+     * @see android.Manifest.permission#NETWORK_SETTINGS
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final long REDACT_FOR_NETWORK_SETTINGS = 1 << 2;
+
+    /**
+     * Redact all fields in this object that require any relevant permission.
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final long REDACT_ALL = -1L;
+
+    /** @hide */
+    @LongDef(flag = true, prefix = { "REDACT_" }, value = {
+            REDACT_NONE,
+            REDACT_FOR_ACCESS_FINE_LOCATION,
+            REDACT_FOR_LOCAL_MAC_ADDRESS,
+            REDACT_FOR_NETWORK_SETTINGS,
+            REDACT_ALL
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RedactionType {}
+
+    // Set to true when private DNS is broken.
+    private boolean mPrivateDnsBroken;
+
+    /**
+     * Uid of the app making the request.
+     */
+    private int mRequestorUid;
+
+    /**
+     * Package name of the app making the request.
+     */
+    private String mRequestorPackageName;
+
+    public NetworkCapabilities() {
+        clearAll();
+        mNetworkCapabilities = DEFAULT_CAPABILITIES;
+    }
+
+    public NetworkCapabilities(NetworkCapabilities nc) {
+        this(nc, REDACT_NONE);
+    }
+
+    /**
+     * Make a copy of NetworkCapabilities.
+     *
+     * @param nc Original NetworkCapabilities
+     * @param redactions bitmask of redactions that needs to be performed on this new instance of
+     *                   {@link NetworkCapabilities}.
+     * @hide
+     */
+    public NetworkCapabilities(@Nullable NetworkCapabilities nc, @RedactionType long redactions) {
+        if (nc != null) {
+            set(nc);
+        }
+        if (mTransportInfo != null) {
+            mTransportInfo = nc.mTransportInfo.makeCopy(redactions);
+        }
+    }
+
+    /**
+     * Completely clears the contents of this object, removing even the capabilities that are set
+     * by default when the object is constructed.
+     * @hide
+     */
+    public void clearAll() {
+        mNetworkCapabilities = mTransportTypes = mForbiddenNetworkCapabilities = 0;
+        mLinkUpBandwidthKbps = mLinkDownBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
+        mNetworkSpecifier = null;
+        mTransportInfo = null;
+        mSignalStrength = SIGNAL_STRENGTH_UNSPECIFIED;
+        mUids = null;
+        mAdministratorUids = new int[0];
+        mOwnerUid = Process.INVALID_UID;
+        mSSID = null;
+        mPrivateDnsBroken = false;
+        mRequestorUid = Process.INVALID_UID;
+        mRequestorPackageName = null;
+        mSubIds = new ArraySet<>();
+    }
+
+    /**
+     * Set all contents of this object to the contents of a NetworkCapabilities.
+     *
+     * @param nc Original NetworkCapabilities
+     * @hide
+     */
+    public void set(@NonNull NetworkCapabilities nc) {
+        mNetworkCapabilities = nc.mNetworkCapabilities;
+        mTransportTypes = nc.mTransportTypes;
+        mLinkUpBandwidthKbps = nc.mLinkUpBandwidthKbps;
+        mLinkDownBandwidthKbps = nc.mLinkDownBandwidthKbps;
+        mNetworkSpecifier = nc.mNetworkSpecifier;
+        if (nc.getTransportInfo() != null) {
+            setTransportInfo(nc.getTransportInfo());
+        } else {
+            setTransportInfo(null);
+        }
+        mSignalStrength = nc.mSignalStrength;
+        mUids = (nc.mUids == null) ? null : new ArraySet<>(nc.mUids);
+        setAdministratorUids(nc.getAdministratorUids());
+        mOwnerUid = nc.mOwnerUid;
+        mForbiddenNetworkCapabilities = nc.mForbiddenNetworkCapabilities;
+        mSSID = nc.mSSID;
+        mPrivateDnsBroken = nc.mPrivateDnsBroken;
+        mRequestorUid = nc.mRequestorUid;
+        mRequestorPackageName = nc.mRequestorPackageName;
+        mSubIds = new ArraySet<>(nc.mSubIds);
+    }
+
+    /**
+     * Represents the network's capabilities.  If any are specified they will be satisfied
+     * by any Network that matches all of them.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    private long mNetworkCapabilities;
+
+    /**
+     * If any capabilities specified here they must not exist in the matching Network.
+     */
+    private long mForbiddenNetworkCapabilities;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "NET_CAPABILITY_" }, value = {
+            NET_CAPABILITY_MMS,
+            NET_CAPABILITY_SUPL,
+            NET_CAPABILITY_DUN,
+            NET_CAPABILITY_FOTA,
+            NET_CAPABILITY_IMS,
+            NET_CAPABILITY_CBS,
+            NET_CAPABILITY_WIFI_P2P,
+            NET_CAPABILITY_IA,
+            NET_CAPABILITY_RCS,
+            NET_CAPABILITY_XCAP,
+            NET_CAPABILITY_EIMS,
+            NET_CAPABILITY_NOT_METERED,
+            NET_CAPABILITY_INTERNET,
+            NET_CAPABILITY_NOT_RESTRICTED,
+            NET_CAPABILITY_TRUSTED,
+            NET_CAPABILITY_NOT_VPN,
+            NET_CAPABILITY_VALIDATED,
+            NET_CAPABILITY_CAPTIVE_PORTAL,
+            NET_CAPABILITY_NOT_ROAMING,
+            NET_CAPABILITY_FOREGROUND,
+            NET_CAPABILITY_NOT_CONGESTED,
+            NET_CAPABILITY_NOT_SUSPENDED,
+            NET_CAPABILITY_OEM_PAID,
+            NET_CAPABILITY_MCX,
+            NET_CAPABILITY_PARTIAL_CONNECTIVITY,
+            NET_CAPABILITY_TEMPORARILY_NOT_METERED,
+            NET_CAPABILITY_OEM_PRIVATE,
+            NET_CAPABILITY_VEHICLE_INTERNAL,
+            NET_CAPABILITY_NOT_VCN_MANAGED,
+            NET_CAPABILITY_ENTERPRISE,
+            NET_CAPABILITY_VSIM,
+            NET_CAPABILITY_BIP,
+    })
+    public @interface NetCapability { }
+
+    /**
+     * Indicates this is a network that has the ability to reach the
+     * carrier's MMSC for sending and receiving MMS messages.
+     */
+    public static final int NET_CAPABILITY_MMS            = 0;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * SUPL server, used to retrieve GPS information.
+     */
+    public static final int NET_CAPABILITY_SUPL           = 1;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * DUN or tethering gateway.
+     */
+    public static final int NET_CAPABILITY_DUN            = 2;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * FOTA portal, used for over the air updates.
+     */
+    public static final int NET_CAPABILITY_FOTA           = 3;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * IMS servers, used for network registration and signaling.
+     */
+    public static final int NET_CAPABILITY_IMS            = 4;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * CBS servers, used for carrier specific services.
+     */
+    public static final int NET_CAPABILITY_CBS            = 5;
+
+    /**
+     * Indicates this is a network that has the ability to reach a Wi-Fi direct
+     * peer.
+     */
+    public static final int NET_CAPABILITY_WIFI_P2P       = 6;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's
+     * Initial Attach servers.
+     */
+    public static final int NET_CAPABILITY_IA             = 7;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's
+     * RCS servers, used for Rich Communication Services.
+     */
+    public static final int NET_CAPABILITY_RCS            = 8;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's
+     * XCAP servers, used for configuration and control.
+     */
+    public static final int NET_CAPABILITY_XCAP           = 9;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's
+     * Emergency IMS servers or other services, used for network signaling
+     * during emergency calls.
+     */
+    public static final int NET_CAPABILITY_EIMS           = 10;
+
+    /**
+     * Indicates that this network is unmetered.
+     */
+    public static final int NET_CAPABILITY_NOT_METERED    = 11;
+
+    /**
+     * Indicates that this network should be able to reach the internet.
+     */
+    public static final int NET_CAPABILITY_INTERNET       = 12;
+
+    /**
+     * Indicates that this network is available for general use.  If this is not set
+     * applications should not attempt to communicate on this network.  Note that this
+     * is simply informative and not enforcement - enforcement is handled via other means.
+     * Set by default.
+     */
+    public static final int NET_CAPABILITY_NOT_RESTRICTED = 13;
+
+    /**
+     * Indicates that the user has indicated implicit trust of this network.  This
+     * generally means it's a sim-selected carrier, a plugged in ethernet, a paired
+     * BT device or a wifi the user asked to connect to.  Untrusted networks
+     * are probably limited to unknown wifi AP.  Set by default.
+     */
+    public static final int NET_CAPABILITY_TRUSTED        = 14;
+
+    /**
+     * Indicates that this network is not a VPN.  This capability is set by default and should be
+     * explicitly cleared for VPN networks.
+     */
+    public static final int NET_CAPABILITY_NOT_VPN        = 15;
+
+    /**
+     * Indicates that connectivity on this network was successfully validated. For example, for a
+     * network with NET_CAPABILITY_INTERNET, it means that Internet connectivity was successfully
+     * detected.
+     */
+    public static final int NET_CAPABILITY_VALIDATED      = 16;
+
+    /**
+     * Indicates that this network was found to have a captive portal in place last time it was
+     * probed.
+     */
+    public static final int NET_CAPABILITY_CAPTIVE_PORTAL = 17;
+
+    /**
+     * Indicates that this network is not roaming.
+     */
+    public static final int NET_CAPABILITY_NOT_ROAMING = 18;
+
+    /**
+     * Indicates that this network is available for use by apps, and not a network that is being
+     * kept up in the background to facilitate fast network switching.
+     */
+    public static final int NET_CAPABILITY_FOREGROUND = 19;
+
+    /**
+     * Indicates that this network is not congested.
+     * <p>
+     * When a network is congested, applications should defer network traffic
+     * that can be done at a later time, such as uploading analytics.
+     */
+    public static final int NET_CAPABILITY_NOT_CONGESTED = 20;
+
+    /**
+     * Indicates that this network is not currently suspended.
+     * <p>
+     * When a network is suspended, the network's IP addresses and any connections
+     * established on the network remain valid, but the network is temporarily unable
+     * to transfer data. This can happen, for example, if a cellular network experiences
+     * a temporary loss of signal, such as when driving through a tunnel, etc.
+     * A network with this capability is not suspended, so is expected to be able to
+     * transfer data.
+     */
+    public static final int NET_CAPABILITY_NOT_SUSPENDED = 21;
+
+    /**
+     * Indicates that traffic that goes through this network is paid by oem. For example,
+     * this network can be used by system apps to upload telemetry data.
+     * @hide
+     */
+    @SystemApi
+    public static final int NET_CAPABILITY_OEM_PAID = 22;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's Mission Critical
+     * servers.
+     */
+    public static final int NET_CAPABILITY_MCX = 23;
+
+    /**
+     * Indicates that this network was tested to only provide partial connectivity.
+     * @hide
+     */
+    @SystemApi
+    public static final int NET_CAPABILITY_PARTIAL_CONNECTIVITY = 24;
+
+    /**
+     * Indicates that this network is temporarily unmetered.
+     * <p>
+     * This capability will be set for networks that are generally metered, but are currently
+     * unmetered, e.g., because the user is in a particular area. This capability can be changed at
+     * any time. When it is removed, applications are responsible for stopping any data transfer
+     * that should not occur on a metered network.
+     * Note that most apps should use {@link #NET_CAPABILITY_NOT_METERED} instead. For more
+     * information, see https://developer.android.com/about/versions/11/features/5g#meteredness.
+     */
+    public static final int NET_CAPABILITY_TEMPORARILY_NOT_METERED = 25;
+
+    /**
+     * Indicates that this network is private to the OEM and meant only for OEM use.
+     * @hide
+     */
+    @SystemApi
+    public static final int NET_CAPABILITY_OEM_PRIVATE = 26;
+
+    /**
+     * Indicates this is an internal vehicle network, meant to communicate with other
+     * automotive systems.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int NET_CAPABILITY_VEHICLE_INTERNAL = 27;
+
+    /**
+     * Indicates that this network is not subsumed by a Virtual Carrier Network (VCN).
+     * <p>
+     * To provide an experience on a VCN similar to a single traditional carrier network, in
+     * some cases the system sets this bit is set by default in application's network requests,
+     * and may choose to remove it at its own discretion when matching the request to a network.
+     * <p>
+     * Applications that want to know about a Virtual Carrier Network's underlying networks,
+     * for example to use them for multipath purposes, should remove this bit from their network
+     * requests ; the system will not add it back once removed.
+     * @hide
+     */
+    @SystemApi
+    public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28;
+
+    /**
+     * Indicates that this network is intended for enterprise use.
+     * <p>
+     * 5G URSP rules may indicate that all data should use a connection dedicated for enterprise
+     * use. If the enterprise capability is requested, all enterprise traffic will be routed over
+     * the connection with this capability.
+     */
+    public static final int NET_CAPABILITY_ENTERPRISE = 29;
+
+    /**
+     * Indicates that this network has ability to access the carrier's Virtual Sim service.
+     * @hide
+     */
+    @SystemApi
+    public static final int NET_CAPABILITY_VSIM = 30;
+
+    /**
+     * Indicates that this network has ability to support Bearer Independent Protol.
+     * @hide
+     */
+    @SystemApi
+    public static final int NET_CAPABILITY_BIP = 31;
+
+    private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS;
+    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_BIP;
+
+    /**
+     * Network capabilities that are expected to be mutable, i.e., can change while a particular
+     * network is connected.
+     */
+    private static final long MUTABLE_CAPABILITIES =
+            // TRUSTED can change when user explicitly connects to an untrusted network in Settings.
+            // http://b/18206275
+            (1 << NET_CAPABILITY_TRUSTED)
+            | (1 << NET_CAPABILITY_VALIDATED)
+            | (1 << NET_CAPABILITY_CAPTIVE_PORTAL)
+            | (1 << NET_CAPABILITY_NOT_ROAMING)
+            | (1 << NET_CAPABILITY_FOREGROUND)
+            | (1 << NET_CAPABILITY_NOT_CONGESTED)
+            | (1 << NET_CAPABILITY_NOT_SUSPENDED)
+            | (1 << NET_CAPABILITY_PARTIAL_CONNECTIVITY)
+            | (1 << NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+            | (1 << NET_CAPABILITY_NOT_VCN_MANAGED);
+
+    /**
+     * Network capabilities that are not allowed in NetworkRequests. This exists because the
+     * NetworkFactory / NetworkAgent model does not deal well with the situation where a
+     * capability's presence cannot be known in advance. If such a capability is requested, then we
+     * can get into a cycle where the NetworkFactory endlessly churns out NetworkAgents that then
+     * get immediately torn down because they do not have the requested capability.
+     */
+    // Note that as a historical exception, the TRUSTED and NOT_VCN_MANAGED capabilities
+    // are mutable but requestable. Factories are responsible for not getting
+    // in an infinite loop about these.
+    private static final long NON_REQUESTABLE_CAPABILITIES =
+            MUTABLE_CAPABILITIES
+            & ~(1 << NET_CAPABILITY_TRUSTED)
+            & ~(1 << NET_CAPABILITY_NOT_VCN_MANAGED);
+
+    /**
+     * Capabilities that are set by default when the object is constructed.
+     */
+    private static final long DEFAULT_CAPABILITIES =
+            (1 << NET_CAPABILITY_NOT_RESTRICTED)
+            | (1 << NET_CAPABILITY_TRUSTED)
+            | (1 << NET_CAPABILITY_NOT_VPN);
+
+    /**
+     * Capabilities that are managed by ConnectivityService.
+     */
+    private static final long CONNECTIVITY_MANAGED_CAPABILITIES =
+            (1 << NET_CAPABILITY_VALIDATED)
+            | (1 << NET_CAPABILITY_CAPTIVE_PORTAL)
+            | (1 << NET_CAPABILITY_FOREGROUND)
+            | (1 << NET_CAPABILITY_PARTIAL_CONNECTIVITY);
+
+    /**
+     * Capabilities that are allowed for test networks. This list must be set so that it is safe
+     * for an unprivileged user to create a network with these capabilities via shell. As such,
+     * it must never contain capabilities that are generally useful to the system, such as
+     * INTERNET, IMS, SUPL, etc.
+     */
+    private static final long TEST_NETWORKS_ALLOWED_CAPABILITIES =
+            (1 << NET_CAPABILITY_NOT_METERED)
+            | (1 << NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+            | (1 << NET_CAPABILITY_NOT_RESTRICTED)
+            | (1 << NET_CAPABILITY_NOT_VPN)
+            | (1 << NET_CAPABILITY_NOT_ROAMING)
+            | (1 << NET_CAPABILITY_NOT_CONGESTED)
+            | (1 << NET_CAPABILITY_NOT_SUSPENDED)
+            | (1 << NET_CAPABILITY_NOT_VCN_MANAGED);
+
+    /**
+     * Adds the given capability to this {@code NetworkCapability} instance.
+     * Note that when searching for a network to satisfy a request, all capabilities
+     * requested must be satisfied.
+     *
+     * @param capability the capability to be added.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities addCapability(@NetCapability int capability) {
+        // If the given capability was previously added to the list of forbidden capabilities
+        // then the capability will also be removed from the list of forbidden capabilities.
+        // TODO: Consider adding forbidden capabilities to the public API and mention this
+        // in the documentation.
+        checkValidCapability(capability);
+        mNetworkCapabilities |= 1L << capability;
+        // remove from forbidden capability list
+        mForbiddenNetworkCapabilities &= ~(1L << capability);
+        return this;
+    }
+
+    /**
+     * Adds the given capability to the list of forbidden capabilities of this
+     * {@code NetworkCapability} instance. Note that when searching for a network to
+     * satisfy a request, the network must not contain any capability from forbidden capability
+     * list.
+     * <p>
+     * If the capability was previously added to the list of required capabilities (for
+     * example, it was there by default or added using {@link #addCapability(int)} method), then
+     * it will be removed from the list of required capabilities as well.
+     *
+     * @see #addCapability(int)
+     * @hide
+     */
+    public void addForbiddenCapability(@NetCapability int capability) {
+        checkValidCapability(capability);
+        mForbiddenNetworkCapabilities |= 1L << capability;
+        mNetworkCapabilities &= ~(1L << capability);  // remove from requested capabilities
+    }
+
+    /**
+     * Removes (if found) the given capability from this {@code NetworkCapability}
+     * instance that were added via addCapability(int) or setCapabilities(int[], int[]).
+     *
+     * @param capability the capability to be removed.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities removeCapability(@NetCapability int capability) {
+        checkValidCapability(capability);
+        final long mask = ~(1L << capability);
+        mNetworkCapabilities &= mask;
+        return this;
+    }
+
+    /**
+     * Removes (if found) the given forbidden capability from this {@code NetworkCapability}
+     * instance that were added via addForbiddenCapability(int) or setCapabilities(int[], int[]).
+     *
+     * @param capability the capability to be removed.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities removeForbiddenCapability(@NetCapability int capability) {
+        checkValidCapability(capability);
+        mForbiddenNetworkCapabilities &= ~(1L << capability);
+        return this;
+    }
+
+    /**
+     * Sets (or clears) the given capability on this {@link NetworkCapabilities}
+     * instance.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setCapability(@NetCapability int capability,
+            boolean value) {
+        if (value) {
+            addCapability(capability);
+        } else {
+            removeCapability(capability);
+        }
+        return this;
+    }
+
+    /**
+     * Gets all the capabilities set on this {@code NetworkCapability} instance.
+     *
+     * @return an array of capability values for this instance.
+     */
+    public @NonNull @NetCapability int[] getCapabilities() {
+        return NetworkCapabilitiesUtils.unpackBits(mNetworkCapabilities);
+    }
+
+    /**
+     * Gets all the forbidden capabilities set on this {@code NetworkCapability} instance.
+     *
+     * @return an array of forbidden capability values for this instance.
+     * @hide
+     */
+    public @NetCapability int[] getForbiddenCapabilities() {
+        return NetworkCapabilitiesUtils.unpackBits(mForbiddenNetworkCapabilities);
+    }
+
+
+    /**
+     * Sets all the capabilities set on this {@code NetworkCapability} instance.
+     * This overwrites any existing capabilities.
+     *
+     * @hide
+     */
+    public void setCapabilities(@NetCapability int[] capabilities,
+            @NetCapability int[] forbiddenCapabilities) {
+        mNetworkCapabilities = NetworkCapabilitiesUtils.packBits(capabilities);
+        mForbiddenNetworkCapabilities = NetworkCapabilitiesUtils.packBits(forbiddenCapabilities);
+    }
+
+    /**
+     * @deprecated use {@link #setCapabilities(int[], int[])}
+     * @hide
+     */
+    @Deprecated
+    public void setCapabilities(@NetCapability int[] capabilities) {
+        setCapabilities(capabilities, new int[] {});
+    }
+
+    /**
+     * Tests for the presence of a capability on this instance.
+     *
+     * @param capability the capabilities to be tested for.
+     * @return {@code true} if set on this instance.
+     */
+    public boolean hasCapability(@NetCapability int capability) {
+        return isValidCapability(capability)
+                && ((mNetworkCapabilities & (1L << capability)) != 0);
+    }
+
+    /** @hide */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public boolean hasForbiddenCapability(@NetCapability int capability) {
+        return isValidCapability(capability)
+                && ((mForbiddenNetworkCapabilities & (1L << capability)) != 0);
+    }
+
+    /**
+     * Check if this NetworkCapabilities has system managed capabilities or not.
+     * @hide
+     */
+    public boolean hasConnectivityManagedCapability() {
+        return ((mNetworkCapabilities & CONNECTIVITY_MANAGED_CAPABILITIES) != 0);
+    }
+
+    /**
+     * Get the name of the given capability that carriers use.
+     * If the capability does not have a carrier-name, returns null.
+     *
+     * @param capability The capability to get the carrier-name of.
+     * @return The carrier-name of the capability, or null if it doesn't exist.
+     * @hide
+     */
+    @SystemApi
+    public static @Nullable String getCapabilityCarrierName(@NetCapability int capability) {
+        if (capability == NET_CAPABILITY_ENTERPRISE) {
+            return capabilityNameOf(capability);
+        } else {
+            return null;
+        }
+    }
+
+    private void combineNetCapabilities(@NonNull NetworkCapabilities nc) {
+        final long wantedCaps = this.mNetworkCapabilities | nc.mNetworkCapabilities;
+        final long forbiddenCaps =
+                this.mForbiddenNetworkCapabilities | nc.mForbiddenNetworkCapabilities;
+        if ((wantedCaps & forbiddenCaps) != 0) {
+            throw new IllegalArgumentException(
+                    "Cannot have the same capability in wanted and forbidden lists.");
+        }
+        this.mNetworkCapabilities = wantedCaps;
+        this.mForbiddenNetworkCapabilities = forbiddenCaps;
+    }
+
+    /**
+     * Convenience function that returns a human-readable description of the first mutable
+     * capability we find. Used to present an error message to apps that request mutable
+     * capabilities.
+     *
+     * @hide
+     */
+    public @Nullable String describeFirstNonRequestableCapability() {
+        final long nonRequestable = (mNetworkCapabilities | mForbiddenNetworkCapabilities)
+                & NON_REQUESTABLE_CAPABILITIES;
+
+        if (nonRequestable != 0) {
+            return capabilityNameOf(NetworkCapabilitiesUtils.unpackBits(nonRequestable)[0]);
+        }
+        if (mLinkUpBandwidthKbps != 0 || mLinkDownBandwidthKbps != 0) return "link bandwidth";
+        if (hasSignalStrength()) return "signalStrength";
+        if (isPrivateDnsBroken()) {
+            return "privateDnsBroken";
+        }
+        return null;
+    }
+
+    private boolean satisfiedByNetCapabilities(@NonNull NetworkCapabilities nc,
+            boolean onlyImmutable) {
+        long requestedCapabilities = mNetworkCapabilities;
+        long requestedForbiddenCapabilities = mForbiddenNetworkCapabilities;
+        long providedCapabilities = nc.mNetworkCapabilities;
+
+        if (onlyImmutable) {
+            requestedCapabilities &= ~MUTABLE_CAPABILITIES;
+            requestedForbiddenCapabilities &= ~MUTABLE_CAPABILITIES;
+        }
+        return ((providedCapabilities & requestedCapabilities) == requestedCapabilities)
+                && ((requestedForbiddenCapabilities & providedCapabilities) == 0);
+    }
+
+    /** @hide */
+    public boolean equalsNetCapabilities(@NonNull NetworkCapabilities nc) {
+        return (nc.mNetworkCapabilities == this.mNetworkCapabilities)
+                && (nc.mForbiddenNetworkCapabilities == this.mForbiddenNetworkCapabilities);
+    }
+
+    private boolean equalsNetCapabilitiesRequestable(@NonNull NetworkCapabilities that) {
+        return ((this.mNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES)
+                == (that.mNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES))
+                && ((this.mForbiddenNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES)
+                == (that.mForbiddenNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES));
+    }
+
+    /**
+     * Removes the NET_CAPABILITY_NOT_RESTRICTED capability if inferring the network is restricted.
+     *
+     * @hide
+     */
+    public void maybeMarkCapabilitiesRestricted() {
+        if (NetworkCapabilitiesUtils.inferRestrictedCapability(this)) {
+            removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        }
+    }
+
+    /**
+     * Test networks have strong restrictions on what capabilities they can have. Enforce these
+     * restrictions.
+     * @hide
+     */
+    public void restrictCapabilitesForTestNetwork(int creatorUid) {
+        final long originalCapabilities = mNetworkCapabilities;
+        final long originalTransportTypes = mTransportTypes;
+        final NetworkSpecifier originalSpecifier = mNetworkSpecifier;
+        final int originalSignalStrength = mSignalStrength;
+        final int originalOwnerUid = getOwnerUid();
+        final int[] originalAdministratorUids = getAdministratorUids();
+        final TransportInfo originalTransportInfo = getTransportInfo();
+        final Set<Integer> originalSubIds = getSubscriptionIds();
+        clearAll();
+        if (0 != (originalCapabilities & NET_CAPABILITY_NOT_RESTRICTED)) {
+            // If the test network is not restricted, then it is only allowed to declare some
+            // specific transports. This is to minimize impact on running apps in case an app
+            // run from the shell creates a test a network.
+            mTransportTypes =
+                    (originalTransportTypes & UNRESTRICTED_TEST_NETWORKS_ALLOWED_TRANSPORTS)
+                            | (1 << TRANSPORT_TEST);
+
+            // SubIds are only allowed for Test Networks that only declare TRANSPORT_TEST.
+            setSubscriptionIds(originalSubIds);
+        } else {
+            // If the test transport is restricted, then it may declare any transport.
+            mTransportTypes = (originalTransportTypes | (1 << TRANSPORT_TEST));
+        }
+        mNetworkCapabilities = originalCapabilities & TEST_NETWORKS_ALLOWED_CAPABILITIES;
+        mNetworkSpecifier = originalSpecifier;
+        mSignalStrength = originalSignalStrength;
+        mTransportInfo = originalTransportInfo;
+
+        // Only retain the owner and administrator UIDs if they match the app registering the remote
+        // caller that registered the network.
+        if (originalOwnerUid == creatorUid) {
+            setOwnerUid(creatorUid);
+        }
+        if (CollectionUtils.contains(originalAdministratorUids, creatorUid)) {
+            setAdministratorUids(new int[] {creatorUid});
+        }
+        // There is no need to clear the UIDs, they have already been cleared by clearAll() above.
+    }
+
+    /**
+     * Representing the transport type.  Apps should generally not care about transport.  A
+     * request for a fast internet connection could be satisfied by a number of different
+     * transports.  If any are specified here it will be satisfied a Network that matches
+     * any of them.  If a caller doesn't care about the transport it should not specify any.
+     */
+    private long mTransportTypes;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "TRANSPORT_" }, value = {
+            TRANSPORT_CELLULAR,
+            TRANSPORT_WIFI,
+            TRANSPORT_BLUETOOTH,
+            TRANSPORT_ETHERNET,
+            TRANSPORT_VPN,
+            TRANSPORT_WIFI_AWARE,
+            TRANSPORT_LOWPAN,
+            TRANSPORT_TEST,
+    })
+    public @interface Transport { }
+
+    /**
+     * Indicates this network uses a Cellular transport.
+     */
+    public static final int TRANSPORT_CELLULAR = 0;
+
+    /**
+     * Indicates this network uses a Wi-Fi transport.
+     */
+    public static final int TRANSPORT_WIFI = 1;
+
+    /**
+     * Indicates this network uses a Bluetooth transport.
+     */
+    public static final int TRANSPORT_BLUETOOTH = 2;
+
+    /**
+     * Indicates this network uses an Ethernet transport.
+     */
+    public static final int TRANSPORT_ETHERNET = 3;
+
+    /**
+     * Indicates this network uses a VPN transport.
+     */
+    public static final int TRANSPORT_VPN = 4;
+
+    /**
+     * Indicates this network uses a Wi-Fi Aware transport.
+     */
+    public static final int TRANSPORT_WIFI_AWARE = 5;
+
+    /**
+     * Indicates this network uses a LoWPAN transport.
+     */
+    public static final int TRANSPORT_LOWPAN = 6;
+
+    /**
+     * Indicates this network uses a Test-only virtual interface as a transport.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public static final int TRANSPORT_TEST = 7;
+
+    /** @hide */
+    public static final int MIN_TRANSPORT = TRANSPORT_CELLULAR;
+    /** @hide */
+    public static final int MAX_TRANSPORT = TRANSPORT_TEST;
+
+    /** @hide */
+    public static boolean isValidTransport(@Transport int transportType) {
+        return (MIN_TRANSPORT <= transportType) && (transportType <= MAX_TRANSPORT);
+    }
+
+    private static final String[] TRANSPORT_NAMES = {
+        "CELLULAR",
+        "WIFI",
+        "BLUETOOTH",
+        "ETHERNET",
+        "VPN",
+        "WIFI_AWARE",
+        "LOWPAN",
+        "TEST"
+    };
+
+    /**
+     * Allowed transports on an unrestricted test network (in addition to TRANSPORT_TEST).
+     */
+    private static final int UNRESTRICTED_TEST_NETWORKS_ALLOWED_TRANSPORTS =
+            1 << TRANSPORT_TEST
+            // Test ethernet networks can be created with EthernetManager#setIncludeTestInterfaces
+            | 1 << TRANSPORT_ETHERNET
+            // Test VPN networks can be created but their UID ranges must be empty.
+            | 1 << TRANSPORT_VPN;
+
+    /**
+     * Adds the given transport type to this {@code NetworkCapability} instance.
+     * Multiple transports may be applied.  Note that when searching
+     * for a network to satisfy a request, any listed in the request will satisfy the request.
+     * For example {@code TRANSPORT_WIFI} and {@code TRANSPORT_ETHERNET} added to a
+     * {@code NetworkCapabilities} would cause either a Wi-Fi network or an Ethernet network
+     * to be selected.  This is logically different than
+     * {@code NetworkCapabilities.NET_CAPABILITY_*} listed above.
+     *
+     * @param transportType the transport type to be added.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities addTransportType(@Transport int transportType) {
+        checkValidTransportType(transportType);
+        mTransportTypes |= 1 << transportType;
+        setNetworkSpecifier(mNetworkSpecifier); // used for exception checking
+        return this;
+    }
+
+    /**
+     * Removes (if found) the given transport from this {@code NetworkCapability} instance.
+     *
+     * @param transportType the transport type to be removed.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities removeTransportType(@Transport int transportType) {
+        checkValidTransportType(transportType);
+        mTransportTypes &= ~(1 << transportType);
+        setNetworkSpecifier(mNetworkSpecifier); // used for exception checking
+        return this;
+    }
+
+    /**
+     * Sets (or clears) the given transport on this {@link NetworkCapabilities}
+     * instance.
+     *
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setTransportType(@Transport int transportType,
+            boolean value) {
+        if (value) {
+            addTransportType(transportType);
+        } else {
+            removeTransportType(transportType);
+        }
+        return this;
+    }
+
+    /**
+     * Gets all the transports set on this {@code NetworkCapability} instance.
+     *
+     * @return an array of transport type values for this instance.
+     * @hide
+     */
+    @SystemApi
+    @NonNull public @Transport int[] getTransportTypes() {
+        return NetworkCapabilitiesUtils.unpackBits(mTransportTypes);
+    }
+
+    /**
+     * Sets all the transports set on this {@code NetworkCapability} instance.
+     * This overwrites any existing transports.
+     *
+     * @hide
+     */
+    public void setTransportTypes(@Transport int[] transportTypes) {
+        mTransportTypes = NetworkCapabilitiesUtils.packBits(transportTypes);
+    }
+
+    /**
+     * Tests for the presence of a transport on this instance.
+     *
+     * @param transportType the transport type to be tested for.
+     * @return {@code true} if set on this instance.
+     */
+    public boolean hasTransport(@Transport int transportType) {
+        return isValidTransport(transportType) && ((mTransportTypes & (1 << transportType)) != 0);
+    }
+
+    private void combineTransportTypes(NetworkCapabilities nc) {
+        this.mTransportTypes |= nc.mTransportTypes;
+    }
+
+    private boolean satisfiedByTransportTypes(NetworkCapabilities nc) {
+        return ((this.mTransportTypes == 0)
+                || ((this.mTransportTypes & nc.mTransportTypes) != 0));
+    }
+
+    /** @hide */
+    public boolean equalsTransportTypes(NetworkCapabilities nc) {
+        return (nc.mTransportTypes == this.mTransportTypes);
+    }
+
+    /**
+     * UID of the app that owns this network, or Process#INVALID_UID if none/unknown.
+     *
+     * <p>This field keeps track of the UID of the app that created this network and is in charge of
+     * its lifecycle. This could be the UID of apps such as the Wifi network suggestor, the running
+     * VPN, or Carrier Service app managing a cellular data connection.
+     *
+     * <p>For NetworkCapability instances being sent from ConnectivityService, this value MUST be
+     * reset to Process.INVALID_UID unless all the following conditions are met:
+     *
+     * <p>The caller is the network owner, AND one of the following sets of requirements is met:
+     *
+     * <ol>
+     *   <li>The described Network is a VPN
+     * </ol>
+     *
+     * <p>OR:
+     *
+     * <ol>
+     *   <li>The calling app is the network owner
+     *   <li>The calling app has the ACCESS_FINE_LOCATION permission granted
+     *   <li>The user's location toggle is on
+     * </ol>
+     *
+     * This is because the owner UID is location-sensitive. The apps that request a network could
+     * know where the device is if they can tell for sure the system has connected to the network
+     * they requested.
+     *
+     * <p>This is populated by the network agents and for the NetworkCapabilities instance sent by
+     * an app to the System Server, the value MUST be reset to Process.INVALID_UID by the system
+     * server.
+     */
+    private int mOwnerUid = Process.INVALID_UID;
+
+    /**
+     * Set the UID of the owner app.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setOwnerUid(final int uid) {
+        mOwnerUid = uid;
+        return this;
+    }
+
+    /**
+     * Retrieves the UID of the app that owns this network.
+     *
+     * <p>For user privacy reasons, this field will only be populated if the following conditions
+     * are met:
+     *
+     * <p>The caller is the network owner, AND one of the following sets of requirements is met:
+     *
+     * <ol>
+     *   <li>The described Network is a VPN
+     * </ol>
+     *
+     * <p>OR:
+     *
+     * <ol>
+     *   <li>The calling app is the network owner
+     *   <li>The calling app has the ACCESS_FINE_LOCATION permission granted
+     *   <li>The user's location toggle is on
+     * </ol>
+     *
+     * Instances of NetworkCapabilities sent to apps without the appropriate permissions will have
+     * this field cleared out.
+     *
+     * <p>
+     * This field will only be populated for VPN and wifi network suggestor apps (i.e using
+     * {@link WifiNetworkSuggestion}), and only for the network they own.
+     * In the case of wifi network suggestors apps, this field is also location sensitive, so the
+     * app needs to hold {@link android.Manifest.permission#ACCESS_FINE_LOCATION} permission. If the
+     * app targets SDK version greater than or equal to {@link Build.VERSION_CODES#S}, then they
+     * also need to use {@link NetworkCallback#FLAG_INCLUDE_LOCATION_INFO} to get the info in their
+     * callback. If the apps targets SDK version equal to {{@link Build.VERSION_CODES#R}, this field
+     * will always be included. The app will be blamed for location access if this field is
+     * included.
+     * </p>
+     */
+    public int getOwnerUid() {
+        return mOwnerUid;
+    }
+
+    private boolean equalsOwnerUid(@NonNull final NetworkCapabilities nc) {
+        return mOwnerUid == nc.mOwnerUid;
+    }
+
+    /**
+     * UIDs of packages that are administrators of this network, or empty if none.
+     *
+     * <p>This field tracks the UIDs of packages that have permission to manage this network.
+     *
+     * <p>Network owners will also be listed as administrators.
+     *
+     * <p>For NetworkCapability instances being sent from the System Server, this value MUST be
+     * empty unless the destination is 1) the System Server, or 2) Telephony. In either case, the
+     * receiving entity must have the ACCESS_FINE_LOCATION permission and target R+.
+     *
+     * <p>When received from an app in a NetworkRequest this is always cleared out by the system
+     * server. This field is never used for matching NetworkRequests to NetworkAgents.
+     */
+    @NonNull private int[] mAdministratorUids = new int[0];
+
+    /**
+     * Sets the int[] of UIDs that are administrators of this network.
+     *
+     * <p>UIDs included in administratorUids gain administrator privileges over this Network.
+     * Examples of UIDs that should be included in administratorUids are:
+     *
+     * <ul>
+     *   <li>Carrier apps with privileges for the relevant subscription
+     *   <li>Active VPN apps
+     *   <li>Other application groups with a particular Network-related role
+     * </ul>
+     *
+     * <p>In general, user-supplied networks (such as WiFi networks) do not have an administrator.
+     *
+     * <p>An app is granted owner privileges over Networks that it supplies. The owner UID MUST
+     * always be included in administratorUids.
+     *
+     * <p>The administrator UIDs are set by network agents.
+     *
+     * @param administratorUids the UIDs to be set as administrators of this Network.
+     * @throws IllegalArgumentException if duplicate UIDs are contained in administratorUids
+     * @see #mAdministratorUids
+     * @hide
+     */
+    @NonNull
+    public NetworkCapabilities setAdministratorUids(@NonNull final int[] administratorUids) {
+        mAdministratorUids = Arrays.copyOf(administratorUids, administratorUids.length);
+        Arrays.sort(mAdministratorUids);
+        for (int i = 0; i < mAdministratorUids.length - 1; i++) {
+            if (mAdministratorUids[i] >= mAdministratorUids[i + 1]) {
+                throw new IllegalArgumentException("All administrator UIDs must be unique");
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Retrieves the UIDs that are administrators of this Network.
+     *
+     * <p>This is only populated in NetworkCapabilities objects that come from network agents for
+     * networks that are managed by specific apps on the system, such as carrier privileged apps or
+     * wifi suggestion apps. This will include the network owner.
+     *
+     * @return the int[] of UIDs that are administrators of this Network
+     * @see #mAdministratorUids
+     * @hide
+     */
+    @NonNull
+    @SystemApi
+    public int[] getAdministratorUids() {
+        return Arrays.copyOf(mAdministratorUids, mAdministratorUids.length);
+    }
+
+    /**
+     * Tests if the set of administrator UIDs of this network is the same as that of the passed one.
+     *
+     * <p>The administrator UIDs must be in sorted order.
+     *
+     * <p>nc is assumed non-null. Else, NPE.
+     *
+     * @hide
+     */
+    @VisibleForTesting(visibility = PRIVATE)
+    public boolean equalsAdministratorUids(@NonNull final NetworkCapabilities nc) {
+        return Arrays.equals(mAdministratorUids, nc.mAdministratorUids);
+    }
+
+    /**
+     * Combine the administrator UIDs of the capabilities.
+     *
+     * <p>This is only legal if either of the administrators lists are empty, or if they are equal.
+     * Combining administrator UIDs is only possible for combining non-overlapping sets of UIDs.
+     *
+     * <p>If both administrator lists are non-empty but not equal, they conflict with each other. In
+     * this case, it would not make sense to add them together.
+     */
+    private void combineAdministratorUids(@NonNull final NetworkCapabilities nc) {
+        if (nc.mAdministratorUids.length == 0) return;
+        if (mAdministratorUids.length == 0) {
+            mAdministratorUids = Arrays.copyOf(nc.mAdministratorUids, nc.mAdministratorUids.length);
+            return;
+        }
+        if (!equalsAdministratorUids(nc)) {
+            throw new IllegalStateException("Can't combine two different administrator UID lists");
+        }
+    }
+
+    /**
+     * Value indicating that link bandwidth is unspecified.
+     * @hide
+     */
+    public static final int LINK_BANDWIDTH_UNSPECIFIED = 0;
+
+    /**
+     * Passive link bandwidth.  This is a rough guide of the expected peak bandwidth
+     * for the first hop on the given transport.  It is not measured, but may take into account
+     * link parameters (Radio technology, allocated channels, etc).
+     */
+    private int mLinkUpBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
+    private int mLinkDownBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
+
+    /**
+     * Sets the upstream bandwidth for this network in Kbps.  This always only refers to
+     * the estimated first hop transport bandwidth.
+     * <p>
+     * {@see Builder#setLinkUpstreamBandwidthKbps}
+     *
+     * @param upKbps the estimated first hop upstream (device to network) bandwidth.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setLinkUpstreamBandwidthKbps(int upKbps) {
+        mLinkUpBandwidthKbps = upKbps;
+        return this;
+    }
+
+    /**
+     * Retrieves the upstream bandwidth for this network in Kbps.  This always only refers to
+     * the estimated first hop transport bandwidth.
+     *
+     * @return The estimated first hop upstream (device to network) bandwidth.
+     */
+    public int getLinkUpstreamBandwidthKbps() {
+        return mLinkUpBandwidthKbps;
+    }
+
+    /**
+     * Sets the downstream bandwidth for this network in Kbps.  This always only refers to
+     * the estimated first hop transport bandwidth.
+     * <p>
+     * {@see Builder#setLinkUpstreamBandwidthKbps}
+     *
+     * @param downKbps the estimated first hop downstream (network to device) bandwidth.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setLinkDownstreamBandwidthKbps(int downKbps) {
+        mLinkDownBandwidthKbps = downKbps;
+        return this;
+    }
+
+    /**
+     * Retrieves the downstream bandwidth for this network in Kbps.  This always only refers to
+     * the estimated first hop transport bandwidth.
+     *
+     * @return The estimated first hop downstream (network to device) bandwidth.
+     */
+    public int getLinkDownstreamBandwidthKbps() {
+        return mLinkDownBandwidthKbps;
+    }
+
+    private void combineLinkBandwidths(NetworkCapabilities nc) {
+        this.mLinkUpBandwidthKbps =
+                Math.max(this.mLinkUpBandwidthKbps, nc.mLinkUpBandwidthKbps);
+        this.mLinkDownBandwidthKbps =
+                Math.max(this.mLinkDownBandwidthKbps, nc.mLinkDownBandwidthKbps);
+    }
+    private boolean satisfiedByLinkBandwidths(NetworkCapabilities nc) {
+        return !(this.mLinkUpBandwidthKbps > nc.mLinkUpBandwidthKbps
+                || this.mLinkDownBandwidthKbps > nc.mLinkDownBandwidthKbps);
+    }
+    private boolean equalsLinkBandwidths(NetworkCapabilities nc) {
+        return (this.mLinkUpBandwidthKbps == nc.mLinkUpBandwidthKbps
+                && this.mLinkDownBandwidthKbps == nc.mLinkDownBandwidthKbps);
+    }
+    /** @hide */
+    public static int minBandwidth(int a, int b) {
+        if (a == LINK_BANDWIDTH_UNSPECIFIED)  {
+            return b;
+        } else if (b == LINK_BANDWIDTH_UNSPECIFIED) {
+            return a;
+        } else {
+            return Math.min(a, b);
+        }
+    }
+    /** @hide */
+    public static int maxBandwidth(int a, int b) {
+        return Math.max(a, b);
+    }
+
+    private NetworkSpecifier mNetworkSpecifier = null;
+    private TransportInfo mTransportInfo = null;
+
+    /**
+     * Sets the optional bearer specific network specifier.
+     * This has no meaning if a single transport is also not specified, so calling
+     * this without a single transport set will generate an exception, as will
+     * subsequently adding or removing transports after this is set.
+     * </p>
+     *
+     * @param networkSpecifier A concrete, parcelable framework class that extends
+     *                         NetworkSpecifier.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setNetworkSpecifier(
+            @NonNull NetworkSpecifier networkSpecifier) {
+        if (networkSpecifier != null && Long.bitCount(mTransportTypes) != 1) {
+            throw new IllegalStateException("Must have a single transport specified to use " +
+                    "setNetworkSpecifier");
+        }
+
+        mNetworkSpecifier = networkSpecifier;
+
+        return this;
+    }
+
+    /**
+     * Sets the optional transport specific information.
+     *
+     * @param transportInfo A concrete, parcelable framework class that extends
+     * {@link TransportInfo}.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setTransportInfo(@NonNull TransportInfo transportInfo) {
+        mTransportInfo = transportInfo;
+        return this;
+    }
+
+    /**
+     * Gets the optional bearer specific network specifier. May be {@code null} if not set.
+     *
+     * @return The optional {@link NetworkSpecifier} specifying the bearer specific network
+     *         specifier or {@code null}.
+     */
+    public @Nullable NetworkSpecifier getNetworkSpecifier() {
+        return mNetworkSpecifier;
+    }
+
+    /**
+     * Returns a transport-specific information container. The application may cast this
+     * container to a concrete sub-class based on its knowledge of the network request. The
+     * application should be able to deal with a {@code null} return value or an invalid case,
+     * e.g. use {@code instanceof} operator to verify expected type.
+     *
+     * @return A concrete implementation of the {@link TransportInfo} class or null if not
+     * available for the network.
+     */
+    @Nullable public TransportInfo getTransportInfo() {
+        return mTransportInfo;
+    }
+
+    private void combineSpecifiers(NetworkCapabilities nc) {
+        if (mNetworkSpecifier != null && !mNetworkSpecifier.equals(nc.mNetworkSpecifier)) {
+            throw new IllegalStateException("Can't combine two networkSpecifiers");
+        }
+        setNetworkSpecifier(nc.mNetworkSpecifier);
+    }
+
+    private boolean satisfiedBySpecifier(NetworkCapabilities nc) {
+        return mNetworkSpecifier == null || mNetworkSpecifier.canBeSatisfiedBy(nc.mNetworkSpecifier)
+                || nc.mNetworkSpecifier instanceof MatchAllNetworkSpecifier;
+    }
+
+    private boolean equalsSpecifier(NetworkCapabilities nc) {
+        return Objects.equals(mNetworkSpecifier, nc.mNetworkSpecifier);
+    }
+
+    private void combineTransportInfos(NetworkCapabilities nc) {
+        if (mTransportInfo != null && !mTransportInfo.equals(nc.mTransportInfo)) {
+            throw new IllegalStateException("Can't combine two TransportInfos");
+        }
+        setTransportInfo(nc.mTransportInfo);
+    }
+
+    private boolean equalsTransportInfo(NetworkCapabilities nc) {
+        return Objects.equals(mTransportInfo, nc.mTransportInfo);
+    }
+
+    /**
+     * Magic value that indicates no signal strength provided. A request specifying this value is
+     * always satisfied.
+     */
+    public static final int SIGNAL_STRENGTH_UNSPECIFIED = Integer.MIN_VALUE;
+
+    /**
+     * Signal strength. This is a signed integer, and higher values indicate better signal.
+     * The exact units are bearer-dependent. For example, Wi-Fi uses RSSI.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    private int mSignalStrength = SIGNAL_STRENGTH_UNSPECIFIED;
+
+    /**
+     * Sets the signal strength. This is a signed integer, with higher values indicating a stronger
+     * signal. The exact units are bearer-dependent. For example, Wi-Fi uses the same RSSI units
+     * reported by wifi code.
+     * <p>
+     * Note that when used to register a network callback, this specifies the minimum acceptable
+     * signal strength. When received as the state of an existing network it specifies the current
+     * value. A value of code SIGNAL_STRENGTH_UNSPECIFIED} means no value when received and has no
+     * effect when requesting a callback.
+     *
+     * @param signalStrength the bearer-specific signal strength.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setSignalStrength(int signalStrength) {
+        mSignalStrength = signalStrength;
+        return this;
+    }
+
+    /**
+     * Returns {@code true} if this object specifies a signal strength.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public boolean hasSignalStrength() {
+        return mSignalStrength > SIGNAL_STRENGTH_UNSPECIFIED;
+    }
+
+    /**
+     * Retrieves the signal strength.
+     *
+     * @return The bearer-specific signal strength.
+     */
+    public int getSignalStrength() {
+        return mSignalStrength;
+    }
+
+    private void combineSignalStrength(NetworkCapabilities nc) {
+        this.mSignalStrength = Math.max(this.mSignalStrength, nc.mSignalStrength);
+    }
+
+    private boolean satisfiedBySignalStrength(NetworkCapabilities nc) {
+        return this.mSignalStrength <= nc.mSignalStrength;
+    }
+
+    private boolean equalsSignalStrength(NetworkCapabilities nc) {
+        return this.mSignalStrength == nc.mSignalStrength;
+    }
+
+    /**
+     * List of UIDs this network applies to. No restriction if null.
+     * <p>
+     * For networks, mUids represent the list of network this applies to, and null means this
+     * network applies to all UIDs.
+     * For requests, mUids is the list of UIDs this network MUST apply to to match ; ALL UIDs
+     * must be included in a network so that they match. As an exception to the general rule,
+     * a null mUids field for requests mean "no requirements" rather than what the general rule
+     * would suggest ("must apply to all UIDs") : this is because this has shown to be what users
+     * of this API expect in practice. A network that must match all UIDs can still be
+     * expressed with a set ranging the entire set of possible UIDs.
+     * <p>
+     * mUids is typically (and at this time, only) used by VPN. This network is only available to
+     * the UIDs in this list, and it is their default network. Apps in this list that wish to
+     * bypass the VPN can do so iff the VPN app allows them to or if they are privileged. If this
+     * member is null, then the network is not restricted by app UID. If it's an empty list, then
+     * it means nobody can use it.
+     * As a special exception, the app managing this network (as identified by its UID stored in
+     * mOwnerUid) can always see this network. This is embodied by a special check in
+     * satisfiedByUids. That still does not mean the network necessarily <strong>applies</strong>
+     * to the app that manages it as determined by #appliesToUid.
+     * <p>
+     * Please note that in principle a single app can be associated with multiple UIDs because
+     * each app will have a different UID when it's run as a different (macro-)user. A single
+     * macro user can only have a single active VPN app at any given time however.
+     * <p>
+     * Also please be aware this class does not try to enforce any normalization on this. Callers
+     * can only alter the UIDs by setting them wholesale : this class does not provide any utility
+     * to add or remove individual UIDs or ranges. If callers have any normalization needs on
+     * their own (like requiring sortedness or no overlap) they need to enforce it
+     * themselves. Some of the internal methods also assume this is normalized as in no adjacent
+     * or overlapping ranges are present.
+     *
+     * @hide
+     */
+    private ArraySet<UidRange> mUids = null;
+
+    /**
+     * Convenience method to set the UIDs this network applies to to a single UID.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setSingleUid(int uid) {
+        mUids = new ArraySet<>(1);
+        mUids.add(new UidRange(uid, uid));
+        return this;
+    }
+
+    /**
+     * Set the list of UIDs this network applies to.
+     * This makes a copy of the set so that callers can't modify it after the call.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setUids(@Nullable Set<Range<Integer>> uids) {
+        mUids = UidRange.fromIntRanges(uids);
+        return this;
+    }
+
+    /**
+     * Get the list of UIDs this network applies to.
+     * This returns a copy of the set so that callers can't modify the original object.
+     *
+     * @return the list of UIDs this network applies to. If {@code null}, then the network applies
+     *         to all UIDs.
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @SuppressLint("NullableCollection")
+    public @Nullable Set<Range<Integer>> getUids() {
+        return UidRange.toIntRanges(mUids);
+    }
+
+    /**
+     * Get the list of UIDs this network applies to.
+     * This returns a copy of the set so that callers can't modify the original object.
+     * @hide
+     */
+    public @Nullable Set<UidRange> getUidRanges() {
+        if (mUids == null) return null;
+
+        return new ArraySet<>(mUids);
+    }
+
+    /**
+     * Test whether this network applies to this UID.
+     * @hide
+     */
+    public boolean appliesToUid(int uid) {
+        if (null == mUids) return true;
+        for (UidRange range : mUids) {
+            if (range.contains(uid)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Compare if the given NetworkCapabilities have the same UIDs.
+     *
+     * @hide
+     */
+    public static boolean hasSameUids(@Nullable NetworkCapabilities nc1,
+            @Nullable NetworkCapabilities nc2) {
+        final Set<UidRange> uids1 = (nc1 == null) ? null : nc1.mUids;
+        final Set<UidRange> uids2 = (nc2 == null) ? null : nc2.mUids;
+        if (null == uids1) return null == uids2;
+        if (null == uids2) return false;
+        // Make a copy so it can be mutated to check that all ranges in uids2 also are in uids.
+        final Set<UidRange> uids = new ArraySet<>(uids2);
+        for (UidRange range : uids1) {
+            if (!uids.contains(range)) {
+                return false;
+            }
+            uids.remove(range);
+        }
+        return uids.isEmpty();
+    }
+
+    /**
+     * Tests if the set of UIDs that this network applies to is the same as the passed network.
+     * <p>
+     * This test only checks whether equal range objects are in both sets. It will
+     * return false if the ranges are not exactly the same, even if the covered UIDs
+     * are for an equivalent result.
+     * <p>
+     * Note that this method is not very optimized, which is fine as long as it's not used very
+     * often.
+     * <p>
+     * nc is assumed nonnull.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public boolean equalsUids(@NonNull NetworkCapabilities nc) {
+        return hasSameUids(nc, this);
+    }
+
+    /**
+     * Test whether the passed NetworkCapabilities satisfies the UIDs this capabilities require.
+     *
+     * This method is called on the NetworkCapabilities embedded in a request with the
+     * capabilities of an available network. It checks whether all the UIDs from this listen
+     * (representing the UIDs that must have access to the network) are satisfied by the UIDs
+     * in the passed nc (representing the UIDs that this network is available to).
+     * <p>
+     * As a special exception, the UID that created the passed network (as represented by its
+     * mOwnerUid field) always satisfies a NetworkRequest requiring it (of LISTEN
+     * or REQUEST types alike), even if the network does not apply to it. That is so a VPN app
+     * can see its own network when it listens for it.
+     * <p>
+     * nc is assumed nonnull. Else, NPE.
+     * @see #appliesToUid
+     * @hide
+     */
+    public boolean satisfiedByUids(@NonNull NetworkCapabilities nc) {
+        if (null == nc.mUids || null == mUids) return true; // The network satisfies everything.
+        for (UidRange requiredRange : mUids) {
+            if (requiredRange.contains(nc.mOwnerUid)) return true;
+            if (!nc.appliesToUidRange(requiredRange)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns whether this network applies to the passed ranges.
+     * This assumes that to apply, the passed range has to be entirely contained
+     * within one of the ranges this network applies to. If the ranges are not normalized,
+     * this method may return false even though all required UIDs are covered because no
+     * single range contained them all.
+     * @hide
+     */
+    @VisibleForTesting
+    public boolean appliesToUidRange(@Nullable UidRange requiredRange) {
+        if (null == mUids) return true;
+        for (UidRange uidRange : mUids) {
+            if (uidRange.containsRange(requiredRange)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Combine the UIDs this network currently applies to with the UIDs the passed
+     * NetworkCapabilities apply to.
+     * nc is assumed nonnull.
+     */
+    private void combineUids(@NonNull NetworkCapabilities nc) {
+        if (null == nc.mUids || null == mUids) {
+            mUids = null;
+            return;
+        }
+        mUids.addAll(nc.mUids);
+    }
+
+
+    /**
+     * The SSID of the network, or null if not applicable or unknown.
+     * <p>
+     * This is filled in by wifi code.
+     * @hide
+     */
+    private String mSSID;
+
+    /**
+     * Sets the SSID of this network.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setSSID(@Nullable String ssid) {
+        mSSID = ssid;
+        return this;
+    }
+
+    /**
+     * Gets the SSID of this network, or null if none or unknown.
+     * @hide
+     */
+    @SystemApi
+    public @Nullable String getSsid() {
+        return mSSID;
+    }
+
+    /**
+     * Tests if the SSID of this network is the same as the SSID of the passed network.
+     * @hide
+     */
+    public boolean equalsSSID(@NonNull NetworkCapabilities nc) {
+        return Objects.equals(mSSID, nc.mSSID);
+    }
+
+    /**
+     * Check if the SSID requirements of this object are matched by the passed object.
+     * @hide
+     */
+    public boolean satisfiedBySSID(@NonNull NetworkCapabilities nc) {
+        return mSSID == null || mSSID.equals(nc.mSSID);
+    }
+
+    /**
+     * Combine SSIDs of the capabilities.
+     * <p>
+     * This is only legal if either the SSID of this object is null, or both SSIDs are
+     * equal.
+     * @hide
+     */
+    private void combineSSIDs(@NonNull NetworkCapabilities nc) {
+        if (mSSID != null && !mSSID.equals(nc.mSSID)) {
+            throw new IllegalStateException("Can't combine two SSIDs");
+        }
+        setSSID(nc.mSSID);
+    }
+
+    /**
+     * Combine a set of Capabilities to this one.  Useful for coming up with the complete set.
+     * <p>
+     * Note that this method may break an invariant of having a particular capability in either
+     * wanted or forbidden lists but never in both.  Requests that have the same capability in
+     * both lists will never be satisfied.
+     * @hide
+     */
+    public void combineCapabilities(@NonNull NetworkCapabilities nc) {
+        combineNetCapabilities(nc);
+        combineTransportTypes(nc);
+        combineLinkBandwidths(nc);
+        combineSpecifiers(nc);
+        combineTransportInfos(nc);
+        combineSignalStrength(nc);
+        combineUids(nc);
+        combineSSIDs(nc);
+        combineRequestor(nc);
+        combineAdministratorUids(nc);
+        combineSubscriptionIds(nc);
+    }
+
+    /**
+     * Check if our requirements are satisfied by the given {@code NetworkCapabilities}.
+     *
+     * @param nc the {@code NetworkCapabilities} that may or may not satisfy our requirements.
+     * @param onlyImmutable if {@code true}, do not consider mutable requirements such as link
+     *         bandwidth, signal strength, or validation / captive portal status.
+     *
+     * @hide
+     */
+    private boolean satisfiedByNetworkCapabilities(NetworkCapabilities nc, boolean onlyImmutable) {
+        return (nc != null
+                && satisfiedByNetCapabilities(nc, onlyImmutable)
+                && satisfiedByTransportTypes(nc)
+                && (onlyImmutable || satisfiedByLinkBandwidths(nc))
+                && satisfiedBySpecifier(nc)
+                && (onlyImmutable || satisfiedBySignalStrength(nc))
+                && (onlyImmutable || satisfiedByUids(nc))
+                && (onlyImmutable || satisfiedBySSID(nc))
+                && (onlyImmutable || satisfiedByRequestor(nc))
+                && (onlyImmutable || satisfiedBySubscriptionIds(nc)));
+    }
+
+    /**
+     * Check if our requirements are satisfied by the given {@code NetworkCapabilities}.
+     *
+     * @param nc the {@code NetworkCapabilities} that may or may not satisfy our requirements.
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean satisfiedByNetworkCapabilities(@Nullable NetworkCapabilities nc) {
+        return satisfiedByNetworkCapabilities(nc, false);
+    }
+
+    /**
+     * Check if our immutable requirements are satisfied by the given {@code NetworkCapabilities}.
+     *
+     * @param nc the {@code NetworkCapabilities} that may or may not satisfy our requirements.
+     *
+     * @hide
+     */
+    public boolean satisfiedByImmutableNetworkCapabilities(@Nullable NetworkCapabilities nc) {
+        return satisfiedByNetworkCapabilities(nc, true);
+    }
+
+    /**
+     * Checks that our immutable capabilities are the same as those of the given
+     * {@code NetworkCapabilities} and return a String describing any difference.
+     * The returned String is empty if there is no difference.
+     *
+     * @hide
+     */
+    public String describeImmutableDifferences(@Nullable NetworkCapabilities that) {
+        if (that == null) {
+            return "other NetworkCapabilities was null";
+        }
+
+        StringJoiner joiner = new StringJoiner(", ");
+
+        // Ignore NOT_METERED being added or removed as it is effectively dynamic. http://b/63326103
+        // TODO: properly support NOT_METERED as a mutable and requestable capability.
+        final long mask = ~MUTABLE_CAPABILITIES & ~(1 << NET_CAPABILITY_NOT_METERED);
+        long oldImmutableCapabilities = this.mNetworkCapabilities & mask;
+        long newImmutableCapabilities = that.mNetworkCapabilities & mask;
+        if (oldImmutableCapabilities != newImmutableCapabilities) {
+            String before = capabilityNamesOf(NetworkCapabilitiesUtils.unpackBits(
+                    oldImmutableCapabilities));
+            String after = capabilityNamesOf(NetworkCapabilitiesUtils.unpackBits(
+                    newImmutableCapabilities));
+            joiner.add(String.format("immutable capabilities changed: %s -> %s", before, after));
+        }
+
+        if (!equalsSpecifier(that)) {
+            NetworkSpecifier before = this.getNetworkSpecifier();
+            NetworkSpecifier after = that.getNetworkSpecifier();
+            joiner.add(String.format("specifier changed: %s -> %s", before, after));
+        }
+
+        if (!equalsTransportTypes(that)) {
+            String before = transportNamesOf(this.getTransportTypes());
+            String after = transportNamesOf(that.getTransportTypes());
+            joiner.add(String.format("transports changed: %s -> %s", before, after));
+        }
+
+        return joiner.toString();
+    }
+
+    /**
+     * Checks that our requestable capabilities are the same as those of the given
+     * {@code NetworkCapabilities}.
+     *
+     * @hide
+     */
+    public boolean equalRequestableCapabilities(@Nullable NetworkCapabilities nc) {
+        if (nc == null) return false;
+        return (equalsNetCapabilitiesRequestable(nc)
+                && equalsTransportTypes(nc)
+                && equalsSpecifier(nc));
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj == null || (obj instanceof NetworkCapabilities == false)) return false;
+        NetworkCapabilities that = (NetworkCapabilities) obj;
+        return equalsNetCapabilities(that)
+                && equalsTransportTypes(that)
+                && equalsLinkBandwidths(that)
+                && equalsSignalStrength(that)
+                && equalsSpecifier(that)
+                && equalsTransportInfo(that)
+                && equalsUids(that)
+                && equalsSSID(that)
+                && equalsOwnerUid(that)
+                && equalsPrivateDnsBroken(that)
+                && equalsRequestor(that)
+                && equalsAdministratorUids(that)
+                && equalsSubscriptionIds(that);
+    }
+
+    @Override
+    public int hashCode() {
+        return (int) (mNetworkCapabilities & 0xFFFFFFFF)
+                + ((int) (mNetworkCapabilities >> 32) * 3)
+                + ((int) (mForbiddenNetworkCapabilities & 0xFFFFFFFF) * 5)
+                + ((int) (mForbiddenNetworkCapabilities >> 32) * 7)
+                + ((int) (mTransportTypes & 0xFFFFFFFF) * 11)
+                + ((int) (mTransportTypes >> 32) * 13)
+                + mLinkUpBandwidthKbps * 17
+                + mLinkDownBandwidthKbps * 19
+                + Objects.hashCode(mNetworkSpecifier) * 23
+                + mSignalStrength * 29
+                + mOwnerUid * 31
+                + Objects.hashCode(mUids) * 37
+                + Objects.hashCode(mSSID) * 41
+                + Objects.hashCode(mTransportInfo) * 43
+                + Objects.hashCode(mPrivateDnsBroken) * 47
+                + Objects.hashCode(mRequestorUid) * 53
+                + Objects.hashCode(mRequestorPackageName) * 59
+                + Arrays.hashCode(mAdministratorUids) * 61
+                + Objects.hashCode(mSubIds) * 67;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    private <T extends Parcelable> void writeParcelableArraySet(Parcel in,
+            @Nullable ArraySet<T> val, int flags) {
+        final int size = (val != null) ? val.size() : -1;
+        in.writeInt(size);
+        for (int i = 0; i < size; i++) {
+            in.writeParcelable(val.valueAt(i), flags);
+        }
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeLong(mNetworkCapabilities);
+        dest.writeLong(mForbiddenNetworkCapabilities);
+        dest.writeLong(mTransportTypes);
+        dest.writeInt(mLinkUpBandwidthKbps);
+        dest.writeInt(mLinkDownBandwidthKbps);
+        dest.writeParcelable((Parcelable) mNetworkSpecifier, flags);
+        dest.writeParcelable((Parcelable) mTransportInfo, flags);
+        dest.writeInt(mSignalStrength);
+        writeParcelableArraySet(dest, mUids, flags);
+        dest.writeString(mSSID);
+        dest.writeBoolean(mPrivateDnsBroken);
+        dest.writeIntArray(getAdministratorUids());
+        dest.writeInt(mOwnerUid);
+        dest.writeInt(mRequestorUid);
+        dest.writeString(mRequestorPackageName);
+        dest.writeIntArray(CollectionUtils.toIntArray(mSubIds));
+    }
+
+    public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
+        new Creator<NetworkCapabilities>() {
+            @Override
+            public NetworkCapabilities createFromParcel(Parcel in) {
+                NetworkCapabilities netCap = new NetworkCapabilities();
+
+                netCap.mNetworkCapabilities = in.readLong();
+                netCap.mForbiddenNetworkCapabilities = in.readLong();
+                netCap.mTransportTypes = in.readLong();
+                netCap.mLinkUpBandwidthKbps = in.readInt();
+                netCap.mLinkDownBandwidthKbps = in.readInt();
+                netCap.mNetworkSpecifier = in.readParcelable(null);
+                netCap.mTransportInfo = in.readParcelable(null);
+                netCap.mSignalStrength = in.readInt();
+                netCap.mUids = readParcelableArraySet(in, null /* ClassLoader, null for default */);
+                netCap.mSSID = in.readString();
+                netCap.mPrivateDnsBroken = in.readBoolean();
+                netCap.setAdministratorUids(in.createIntArray());
+                netCap.mOwnerUid = in.readInt();
+                netCap.mRequestorUid = in.readInt();
+                netCap.mRequestorPackageName = in.readString();
+                netCap.mSubIds = new ArraySet<>();
+                final int[] subIdInts = Objects.requireNonNull(in.createIntArray());
+                for (int i = 0; i < subIdInts.length; i++) {
+                    netCap.mSubIds.add(subIdInts[i]);
+                }
+                return netCap;
+            }
+            @Override
+            public NetworkCapabilities[] newArray(int size) {
+                return new NetworkCapabilities[size];
+            }
+
+            private @Nullable <T extends Parcelable> ArraySet<T> readParcelableArraySet(Parcel in,
+                    @Nullable ClassLoader loader) {
+                final int size = in.readInt();
+                if (size < 0) {
+                    return null;
+                }
+                final ArraySet<T> result = new ArraySet<>(size);
+                for (int i = 0; i < size; i++) {
+                    final T value = in.readParcelable(loader);
+                    result.add(value);
+                }
+                return result;
+            }
+        };
+
+    @Override
+    public @NonNull String toString() {
+        final StringBuilder sb = new StringBuilder("[");
+        if (0 != mTransportTypes) {
+            sb.append(" Transports: ");
+            appendStringRepresentationOfBitMaskToStringBuilder(sb, mTransportTypes,
+                    NetworkCapabilities::transportNameOf, "|");
+        }
+        if (0 != mNetworkCapabilities) {
+            sb.append(" Capabilities: ");
+            appendStringRepresentationOfBitMaskToStringBuilder(sb, mNetworkCapabilities,
+                    NetworkCapabilities::capabilityNameOf, "&");
+        }
+        if (0 != mForbiddenNetworkCapabilities) {
+            sb.append(" Forbidden: ");
+            appendStringRepresentationOfBitMaskToStringBuilder(sb, mForbiddenNetworkCapabilities,
+                    NetworkCapabilities::capabilityNameOf, "&");
+        }
+        if (mLinkUpBandwidthKbps > 0) {
+            sb.append(" LinkUpBandwidth>=").append(mLinkUpBandwidthKbps).append("Kbps");
+        }
+        if (mLinkDownBandwidthKbps > 0) {
+            sb.append(" LinkDnBandwidth>=").append(mLinkDownBandwidthKbps).append("Kbps");
+        }
+        if (mNetworkSpecifier != null) {
+            sb.append(" Specifier: <").append(mNetworkSpecifier).append(">");
+        }
+        if (mTransportInfo != null) {
+            sb.append(" TransportInfo: <").append(mTransportInfo).append(">");
+        }
+        if (hasSignalStrength()) {
+            sb.append(" SignalStrength: ").append(mSignalStrength);
+        }
+
+        if (null != mUids) {
+            if ((1 == mUids.size()) && (mUids.valueAt(0).count() == 1)) {
+                sb.append(" Uid: ").append(mUids.valueAt(0).start);
+            } else {
+                sb.append(" Uids: <").append(mUids).append(">");
+            }
+        }
+        if (mOwnerUid != Process.INVALID_UID) {
+            sb.append(" OwnerUid: ").append(mOwnerUid);
+        }
+
+        if (mAdministratorUids != null && mAdministratorUids.length != 0) {
+            sb.append(" AdminUids: ").append(Arrays.toString(mAdministratorUids));
+        }
+
+        if (mRequestorUid != Process.INVALID_UID) {
+            sb.append(" RequestorUid: ").append(mRequestorUid);
+        }
+
+        if (mRequestorPackageName != null) {
+            sb.append(" RequestorPkg: ").append(mRequestorPackageName);
+        }
+
+        if (null != mSSID) {
+            sb.append(" SSID: ").append(mSSID);
+        }
+
+        if (mPrivateDnsBroken) {
+            sb.append(" PrivateDnsBroken");
+        }
+
+        if (!mSubIds.isEmpty()) {
+            sb.append(" SubscriptionIds: ").append(mSubIds);
+        }
+
+        sb.append("]");
+        return sb.toString();
+    }
+
+
+    private interface NameOf {
+        String nameOf(int value);
+    }
+
+    /**
+     * @hide
+     */
+    public static void appendStringRepresentationOfBitMaskToStringBuilder(@NonNull StringBuilder sb,
+            long bitMask, @NonNull NameOf nameFetcher, @NonNull String separator) {
+        int bitPos = 0;
+        boolean firstElementAdded = false;
+        while (bitMask != 0) {
+            if ((bitMask & 1) != 0) {
+                if (firstElementAdded) {
+                    sb.append(separator);
+                } else {
+                    firstElementAdded = true;
+                }
+                sb.append(nameFetcher.nameOf(bitPos));
+            }
+            bitMask >>= 1;
+            ++bitPos;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public static @NonNull String capabilityNamesOf(@Nullable @NetCapability int[] capabilities) {
+        StringJoiner joiner = new StringJoiner("|");
+        if (capabilities != null) {
+            for (int c : capabilities) {
+                joiner.add(capabilityNameOf(c));
+            }
+        }
+        return joiner.toString();
+    }
+
+    /**
+     * @hide
+     */
+    public static @NonNull String capabilityNameOf(@NetCapability int capability) {
+        switch (capability) {
+            case NET_CAPABILITY_MMS:                  return "MMS";
+            case NET_CAPABILITY_SUPL:                 return "SUPL";
+            case NET_CAPABILITY_DUN:                  return "DUN";
+            case NET_CAPABILITY_FOTA:                 return "FOTA";
+            case NET_CAPABILITY_IMS:                  return "IMS";
+            case NET_CAPABILITY_CBS:                  return "CBS";
+            case NET_CAPABILITY_WIFI_P2P:             return "WIFI_P2P";
+            case NET_CAPABILITY_IA:                   return "IA";
+            case NET_CAPABILITY_RCS:                  return "RCS";
+            case NET_CAPABILITY_XCAP:                 return "XCAP";
+            case NET_CAPABILITY_EIMS:                 return "EIMS";
+            case NET_CAPABILITY_NOT_METERED:          return "NOT_METERED";
+            case NET_CAPABILITY_INTERNET:             return "INTERNET";
+            case NET_CAPABILITY_NOT_RESTRICTED:       return "NOT_RESTRICTED";
+            case NET_CAPABILITY_TRUSTED:              return "TRUSTED";
+            case NET_CAPABILITY_NOT_VPN:              return "NOT_VPN";
+            case NET_CAPABILITY_VALIDATED:            return "VALIDATED";
+            case NET_CAPABILITY_CAPTIVE_PORTAL:       return "CAPTIVE_PORTAL";
+            case NET_CAPABILITY_NOT_ROAMING:          return "NOT_ROAMING";
+            case NET_CAPABILITY_FOREGROUND:           return "FOREGROUND";
+            case NET_CAPABILITY_NOT_CONGESTED:        return "NOT_CONGESTED";
+            case NET_CAPABILITY_NOT_SUSPENDED:        return "NOT_SUSPENDED";
+            case NET_CAPABILITY_OEM_PAID:             return "OEM_PAID";
+            case NET_CAPABILITY_MCX:                  return "MCX";
+            case NET_CAPABILITY_PARTIAL_CONNECTIVITY: return "PARTIAL_CONNECTIVITY";
+            case NET_CAPABILITY_TEMPORARILY_NOT_METERED:    return "TEMPORARILY_NOT_METERED";
+            case NET_CAPABILITY_OEM_PRIVATE:          return "OEM_PRIVATE";
+            case NET_CAPABILITY_VEHICLE_INTERNAL:     return "VEHICLE_INTERNAL";
+            case NET_CAPABILITY_NOT_VCN_MANAGED:      return "NOT_VCN_MANAGED";
+            case NET_CAPABILITY_ENTERPRISE:           return "ENTERPRISE";
+            case NET_CAPABILITY_VSIM:                 return "VSIM";
+            case NET_CAPABILITY_BIP:                  return "BIP";
+            default:                                  return Integer.toString(capability);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static @NonNull String transportNamesOf(@Nullable @Transport int[] types) {
+        StringJoiner joiner = new StringJoiner("|");
+        if (types != null) {
+            for (int t : types) {
+                joiner.add(transportNameOf(t));
+            }
+        }
+        return joiner.toString();
+    }
+
+    /**
+     * @hide
+     */
+    public static @NonNull String transportNameOf(@Transport int transport) {
+        if (!isValidTransport(transport)) {
+            return "UNKNOWN";
+        }
+        return TRANSPORT_NAMES[transport];
+    }
+
+    private static void checkValidTransportType(@Transport int transport) {
+        if (!isValidTransport(transport)) {
+            throw new IllegalArgumentException("Invalid TransportType " + transport);
+        }
+    }
+
+    private static boolean isValidCapability(@NetworkCapabilities.NetCapability int capability) {
+        return capability >= MIN_NET_CAPABILITY && capability <= MAX_NET_CAPABILITY;
+    }
+
+    private static void checkValidCapability(@NetworkCapabilities.NetCapability int capability) {
+        if (!isValidCapability(capability)) {
+            throw new IllegalArgumentException("NetworkCapability " + capability + "out of range");
+        }
+    }
+
+    /**
+     * Check if this {@code NetworkCapability} instance is metered.
+     *
+     * @return {@code true} if {@code NET_CAPABILITY_NOT_METERED} is not set on this instance.
+     * @hide
+     */
+    public boolean isMetered() {
+        return !hasCapability(NET_CAPABILITY_NOT_METERED);
+    }
+
+    /**
+     * Check if private dns is broken.
+     *
+     * @return {@code true} if private DNS is broken on this network.
+     * @hide
+     */
+    @SystemApi
+    public boolean isPrivateDnsBroken() {
+        return mPrivateDnsBroken;
+    }
+
+    /**
+     * Set mPrivateDnsBroken to true when private dns is broken.
+     *
+     * @param broken the status of private DNS to be set.
+     * @hide
+     */
+    public void setPrivateDnsBroken(boolean broken) {
+        mPrivateDnsBroken = broken;
+    }
+
+    private boolean equalsPrivateDnsBroken(NetworkCapabilities nc) {
+        return mPrivateDnsBroken == nc.mPrivateDnsBroken;
+    }
+
+    /**
+     * Set the UID of the app making the request.
+     *
+     * For instances of NetworkCapabilities representing a request, sets the
+     * UID of the app making the request. For a network created by the system,
+     * sets the UID of the only app whose requests can match this network.
+     * This can be set to {@link Process#INVALID_UID} if there is no such app,
+     * or if this instance of NetworkCapabilities is about to be sent to a
+     * party that should not learn about this.
+     *
+     * @param uid UID of the app.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setRequestorUid(int uid) {
+        mRequestorUid = uid;
+        return this;
+    }
+
+    /**
+     * Returns the UID of the app making the request.
+     *
+     * For a NetworkRequest being made by an app, contains the app's UID. For a network
+     * created by the system, contains the UID of the only app whose requests can match
+     * this network, or {@link Process#INVALID_UID} if none or if the
+     * caller does not have permission to learn about this.
+     *
+     * @return the uid of the app making the request.
+     * @hide
+     */
+    public int getRequestorUid() {
+        return mRequestorUid;
+    }
+
+    /**
+     * Set the package name of the app making the request.
+     *
+     * For instances of NetworkCapabilities representing a request, sets the
+     * package name of the app making the request. For a network created by the system,
+     * sets the package name of the only app whose requests can match this network.
+     * This can be set to null if there is no such app, or if this instance of
+     * NetworkCapabilities is about to be sent to a party that should not learn about this.
+     *
+     * @param packageName package name of the app.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setRequestorPackageName(@NonNull String packageName) {
+        mRequestorPackageName = packageName;
+        return this;
+    }
+
+    /**
+     * Returns the package name of the app making the request.
+     *
+     * For a NetworkRequest being made by an app, contains the app's package name. For a
+     * network created by the system, contains the package name of the only app whose
+     * requests can match this network, or null if none or if the caller does not have
+     * permission to learn about this.
+     *
+     * @return the package name of the app making the request.
+     * @hide
+     */
+    @Nullable
+    public String getRequestorPackageName() {
+        return mRequestorPackageName;
+    }
+
+    /**
+     * Set the uid and package name of the app causing this network to exist.
+     *
+     * {@see #setRequestorUid} and {@link #setRequestorPackageName}
+     *
+     * @param uid UID of the app.
+     * @param packageName package name of the app.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setRequestorUidAndPackageName(
+            int uid, @NonNull String packageName) {
+        return setRequestorUid(uid).setRequestorPackageName(packageName);
+    }
+
+    /**
+     * Test whether the passed NetworkCapabilities satisfies the requestor restrictions of this
+     * capabilities.
+     *
+     * This method is called on the NetworkCapabilities embedded in a request with the
+     * capabilities of an available network. If the available network, sets a specific
+     * requestor (by uid and optionally package name), then this will only match a request from the
+     * same app. If either of the capabilities have an unset uid or package name, then it matches
+     * everything.
+     * <p>
+     * nc is assumed nonnull. Else, NPE.
+     */
+    private boolean satisfiedByRequestor(NetworkCapabilities nc) {
+        // No uid set, matches everything.
+        if (mRequestorUid == Process.INVALID_UID || nc.mRequestorUid == Process.INVALID_UID) {
+            return true;
+        }
+        // uids don't match.
+        if (mRequestorUid != nc.mRequestorUid) return false;
+        // No package names set, matches everything
+        if (null == nc.mRequestorPackageName || null == mRequestorPackageName) return true;
+        // check for package name match.
+        return TextUtils.equals(mRequestorPackageName, nc.mRequestorPackageName);
+    }
+
+    /**
+     * Combine requestor info of the capabilities.
+     * <p>
+     * This is only legal if either the requestor info of this object is reset, or both info are
+     * equal.
+     * nc is assumed nonnull.
+     */
+    private void combineRequestor(@NonNull NetworkCapabilities nc) {
+        if (mRequestorUid != Process.INVALID_UID && mRequestorUid != nc.mOwnerUid) {
+            throw new IllegalStateException("Can't combine two uids");
+        }
+        if (mRequestorPackageName != null
+                && !mRequestorPackageName.equals(nc.mRequestorPackageName)) {
+            throw new IllegalStateException("Can't combine two package names");
+        }
+        setRequestorUid(nc.mRequestorUid);
+        setRequestorPackageName(nc.mRequestorPackageName);
+    }
+
+    private boolean equalsRequestor(NetworkCapabilities nc) {
+        return mRequestorUid == nc.mRequestorUid
+                && TextUtils.equals(mRequestorPackageName, nc.mRequestorPackageName);
+    }
+
+    /**
+     * Set of the subscription IDs that identifies the network or request, empty if none.
+     */
+    @NonNull
+    private ArraySet<Integer> mSubIds = new ArraySet<>();
+
+    /**
+     * Sets the subscription ID set that associated to this network or request.
+     *
+     * @hide
+     */
+    @NonNull
+    public NetworkCapabilities setSubscriptionIds(@NonNull Set<Integer> subIds) {
+        mSubIds = new ArraySet(Objects.requireNonNull(subIds));
+        return this;
+    }
+
+    /**
+     * Gets the subscription ID set that associated to this network or request.
+     *
+     * <p>Instances of NetworkCapabilities will only have this field populated by the system if the
+     * receiver holds the NETWORK_FACTORY permission. In all other cases, it will be the empty set.
+     *
+     * @return
+     * @hide
+     */
+    @NonNull
+    @SystemApi
+    public Set<Integer> getSubscriptionIds() {
+        return new ArraySet<>(mSubIds);
+    }
+
+    /**
+     * Tests if the subscription ID set of this network is the same as that of the passed one.
+     */
+    private boolean equalsSubscriptionIds(@NonNull NetworkCapabilities nc) {
+        return Objects.equals(mSubIds, nc.mSubIds);
+    }
+
+    /**
+     * Check if the subscription ID set requirements of this object are matched by the passed one.
+     * If specified in the request, the passed one need to have at least one subId and at least
+     * one of them needs to be in the request set.
+     */
+    private boolean satisfiedBySubscriptionIds(@NonNull NetworkCapabilities nc) {
+        if (mSubIds.isEmpty()) return true;
+        if (nc.mSubIds.isEmpty()) return false;
+        for (final Integer subId : nc.mSubIds) {
+            if (mSubIds.contains(subId)) return true;
+        }
+        return false;
+    }
+
+    /**
+     * Combine subscription ID set of the capabilities.
+     *
+     * <p>This is only legal if the subscription Ids are equal.
+     *
+     * <p>If both subscription IDs are not equal, they belong to different subscription
+     * (or no subscription). In this case, it would not make sense to add them together.
+     */
+    private void combineSubscriptionIds(@NonNull NetworkCapabilities nc) {
+        if (!Objects.equals(mSubIds, nc.mSubIds)) {
+            throw new IllegalStateException("Can't combine two subscription ID sets");
+        }
+    }
+
+    /**
+     * Returns a bitmask of all the applicable redactions (based on the permissions held by the
+     * receiving app) to be performed on this object.
+     *
+     * @return bitmask of redactions applicable on this instance.
+     * @hide
+     */
+    public @RedactionType long getApplicableRedactions() {
+        // Currently, there are no fields redacted in NetworkCapabilities itself, so we just
+        // passthrough the redactions required by the embedded TransportInfo. If this changes
+        // in the future, modify this method.
+        if (mTransportInfo == null) {
+            return NetworkCapabilities.REDACT_NONE;
+        }
+        return mTransportInfo.getApplicableRedactions();
+    }
+
+    private NetworkCapabilities removeDefaultCapabilites() {
+        mNetworkCapabilities &= ~DEFAULT_CAPABILITIES;
+        return this;
+    }
+
+    /**
+     * Builder class for NetworkCapabilities.
+     *
+     * This class is mainly for 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
+     * enforce these restrictions itself but the system server clears out the relevant
+     * fields when receiving a NetworkCapabilities object from a caller without the
+     * appropriate permission.
+     *
+     * Apps don't use this builder directly. Instead, they use {@link NetworkRequest} via
+     * its builder object.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final class Builder {
+        private final NetworkCapabilities mCaps;
+
+        /**
+         * Creates a new Builder to construct NetworkCapabilities objects.
+         */
+        public Builder() {
+            mCaps = new NetworkCapabilities();
+        }
+
+        /**
+         * Creates a new Builder of NetworkCapabilities from an existing instance.
+         */
+        public Builder(@NonNull final NetworkCapabilities nc) {
+            Objects.requireNonNull(nc);
+            mCaps = new NetworkCapabilities(nc);
+        }
+
+        /**
+         * Creates a new Builder without the default capabilities.
+         */
+        @NonNull
+        public static Builder withoutDefaultCapabilities() {
+            final NetworkCapabilities nc = new NetworkCapabilities();
+            nc.removeDefaultCapabilites();
+            return new Builder(nc);
+        }
+
+        /**
+         * Adds the given transport type.
+         *
+         * Multiple transports may be added. Note that when searching for a network to satisfy a
+         * request, satisfying any of the transports listed in the request will satisfy the request.
+         * For example {@code TRANSPORT_WIFI} and {@code TRANSPORT_ETHERNET} added to a
+         * {@code NetworkCapabilities} would cause either a Wi-Fi network or an Ethernet network
+         * to be selected. This is logically different than
+         * {@code NetworkCapabilities.NET_CAPABILITY_*}. Also note that multiple networks with the
+         * same transport type may be active concurrently.
+         *
+         * @param transportType the transport type to be added or removed.
+         * @return this builder
+         */
+        @NonNull
+        public Builder addTransportType(@Transport int transportType) {
+            checkValidTransportType(transportType);
+            mCaps.addTransportType(transportType);
+            return this;
+        }
+
+        /**
+         * Removes the given transport type.
+         *
+         * {@see #addTransportType}.
+         *
+         * @param transportType the transport type to be added or removed.
+         * @return this builder
+         */
+        @NonNull
+        public Builder removeTransportType(@Transport int transportType) {
+            checkValidTransportType(transportType);
+            mCaps.removeTransportType(transportType);
+            return this;
+        }
+
+        /**
+         * Adds the given capability.
+         *
+         * @param capability the capability
+         * @return this builder
+         */
+        @NonNull
+        public Builder addCapability(@NetCapability final int capability) {
+            mCaps.setCapability(capability, true);
+            return this;
+        }
+
+        /**
+         * Removes the given capability.
+         *
+         * @param capability the capability
+         * @return this builder
+         */
+        @NonNull
+        public Builder removeCapability(@NetCapability final int capability) {
+            mCaps.setCapability(capability, false);
+            return this;
+        }
+
+        /**
+         * Sets the owner UID.
+         *
+         * The default value is {@link Process#INVALID_UID}. Pass this value to reset.
+         *
+         * Note: for security the system will clear out this field when received from a
+         * non-privileged source.
+         *
+         * @param ownerUid the owner UID
+         * @return this builder
+         */
+        @NonNull
+        @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
+        public Builder setOwnerUid(final int ownerUid) {
+            mCaps.setOwnerUid(ownerUid);
+            return this;
+        }
+
+        /**
+         * Sets the list of UIDs that are administrators of this network.
+         *
+         * <p>UIDs included in administratorUids gain administrator privileges over this
+         * Network. Examples of UIDs that should be included in administratorUids are:
+         * <ul>
+         *     <li>Carrier apps with privileges for the relevant subscription
+         *     <li>Active VPN apps
+         *     <li>Other application groups with a particular Network-related role
+         * </ul>
+         *
+         * <p>In general, user-supplied networks (such as WiFi networks) do not have
+         * administrators.
+         *
+         * <p>An app is granted owner privileges over Networks that it supplies. The owner
+         * UID MUST always be included in administratorUids.
+         *
+         * The default value is the empty array. Pass an empty array to reset.
+         *
+         * Note: for security the system will clear out this field when received from a
+         * non-privileged source, such as an app using reflection to call this or
+         * mutate the member in the built object.
+         *
+         * @param administratorUids the UIDs to be set as administrators of this Network.
+         * @return this builder
+         */
+        @NonNull
+        @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
+        public Builder setAdministratorUids(@NonNull final int[] administratorUids) {
+            Objects.requireNonNull(administratorUids);
+            mCaps.setAdministratorUids(administratorUids);
+            return this;
+        }
+
+        /**
+         * Sets the upstream bandwidth of the link.
+         *
+         * Sets the upstream bandwidth for this network in Kbps. This always only refers to
+         * the estimated first hop transport bandwidth.
+         * <p>
+         * Note that when used to request a network, this specifies the minimum acceptable.
+         * When received as the state of an existing network this specifies the typical
+         * first hop bandwidth expected. This is never measured, but rather is inferred
+         * from technology type and other link parameters. It could be used to differentiate
+         * between very slow 1xRTT cellular links and other faster networks or even between
+         * 802.11b vs 802.11AC wifi technologies. It should not be used to differentiate between
+         * fast backhauls and slow backhauls.
+         *
+         * @param upKbps the estimated first hop upstream (device to network) bandwidth.
+         * @return this builder
+         */
+        @NonNull
+        public Builder setLinkUpstreamBandwidthKbps(final int upKbps) {
+            mCaps.setLinkUpstreamBandwidthKbps(upKbps);
+            return this;
+        }
+
+        /**
+         * Sets the downstream bandwidth for this network in Kbps. This always only refers to
+         * the estimated first hop transport bandwidth.
+         * <p>
+         * Note that when used to request a network, this specifies the minimum acceptable.
+         * When received as the state of an existing network this specifies the typical
+         * first hop bandwidth expected. This is never measured, but rather is inferred
+         * from technology type and other link parameters. It could be used to differentiate
+         * between very slow 1xRTT cellular links and other faster networks or even between
+         * 802.11b vs 802.11AC wifi technologies. It should not be used to differentiate between
+         * fast backhauls and slow backhauls.
+         *
+         * @param downKbps the estimated first hop downstream (network to device) bandwidth.
+         * @return this builder
+         */
+        @NonNull
+        public Builder setLinkDownstreamBandwidthKbps(final int downKbps) {
+            mCaps.setLinkDownstreamBandwidthKbps(downKbps);
+            return this;
+        }
+
+        /**
+         * Sets the optional bearer specific network specifier.
+         * This has no meaning if a single transport is also not specified, so calling
+         * this without a single transport set will generate an exception, as will
+         * subsequently adding or removing transports after this is set.
+         * </p>
+         *
+         * @param specifier a concrete, parcelable framework class that extends NetworkSpecifier,
+         *        or null to clear it.
+         * @return this builder
+         */
+        @NonNull
+        public Builder setNetworkSpecifier(@Nullable final NetworkSpecifier specifier) {
+            mCaps.setNetworkSpecifier(specifier);
+            return this;
+        }
+
+        /**
+         * Sets the optional transport specific information.
+         *
+         * @param info A concrete, parcelable framework class that extends {@link TransportInfo},
+         *             or null to clear it.
+         * @return this builder
+         */
+        @NonNull
+        public Builder setTransportInfo(@Nullable final TransportInfo info) {
+            mCaps.setTransportInfo(info);
+            return this;
+        }
+
+        /**
+         * Sets the signal strength. This is a signed integer, with higher values indicating a
+         * stronger signal. The exact units are bearer-dependent. For example, Wi-Fi uses the
+         * same RSSI units reported by wifi code.
+         * <p>
+         * Note that when used to register a network callback, this specifies the minimum
+         * acceptable signal strength. When received as the state of an existing network it
+         * specifies the current value. A value of code SIGNAL_STRENGTH_UNSPECIFIED} means
+         * no value when received and has no effect when requesting a callback.
+         *
+         * Note: for security the system will throw if it receives a NetworkRequest where
+         * the underlying NetworkCapabilities has this member set from a source that does
+         * not hold the {@link android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP}
+         * permission. Apps with this permission can use this indirectly through
+         * {@link android.net.NetworkRequest}.
+         *
+         * @param signalStrength the bearer-specific signal strength.
+         * @return this builder
+         */
+        @NonNull
+        @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP)
+        public Builder setSignalStrength(final int signalStrength) {
+            mCaps.setSignalStrength(signalStrength);
+            return this;
+        }
+
+        /**
+         * Sets the SSID of this network.
+         *
+         * Note: for security the system will clear out this field when received from a
+         * non-privileged source, like an app using reflection to set this.
+         *
+         * @param ssid the SSID, or null to clear it.
+         * @return this builder
+         */
+        @NonNull
+        @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
+        public Builder setSsid(@Nullable final String ssid) {
+            mCaps.setSSID(ssid);
+            return this;
+        }
+
+        /**
+         * Set the uid of the app causing this network to exist.
+         *
+         * Note: for security the system will clear out this field when received from a
+         * non-privileged source.
+         *
+         * @param uid UID of the app.
+         * @return this builder
+         */
+        @NonNull
+        @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
+        public Builder setRequestorUid(final int uid) {
+            mCaps.setRequestorUid(uid);
+            return this;
+        }
+
+        /**
+         * Set the package name of the app causing this network to exist.
+         *
+         * Note: for security the system will clear out this field when received from a
+         * non-privileged source.
+         *
+         * @param packageName package name of the app, or null to clear it.
+         * @return this builder
+         */
+        @NonNull
+        @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
+        public Builder setRequestorPackageName(@Nullable final String packageName) {
+            mCaps.setRequestorPackageName(packageName);
+            return this;
+        }
+
+        /**
+         * Set the subscription ID set.
+         *
+         * <p>SubIds are populated in NetworkCapability instances from the system only for callers
+         * that hold the NETWORK_FACTORY permission. Similarly, the system will reject any
+         * NetworkRequests filed with a non-empty set of subIds unless the caller holds the
+         * NETWORK_FACTORY permission.
+         *
+         * @param subIds a set that represent the subscription IDs. Empty if clean up.
+         * @return this builder.
+         * @hide
+         */
+        @NonNull
+        @SystemApi
+        public Builder setSubscriptionIds(@NonNull final Set<Integer> subIds) {
+            mCaps.setSubscriptionIds(subIds);
+            return this;
+        }
+
+        /**
+         * Set the list of UIDs this network applies to.
+         *
+         * @param uids the list of UIDs this network applies to, or {@code null} if this network
+         *             applies to all UIDs.
+         * @return this builder
+         * @hide
+         */
+        @NonNull
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+        public Builder setUids(@Nullable Set<Range<Integer>> uids) {
+            mCaps.setUids(uids);
+            return this;
+        }
+
+        /**
+         * Builds the instance of the capabilities.
+         *
+         * @return the built instance of NetworkCapabilities.
+         */
+        @NonNull
+        public NetworkCapabilities build() {
+            if (mCaps.getOwnerUid() != Process.INVALID_UID) {
+                if (!CollectionUtils.contains(mCaps.getAdministratorUids(), mCaps.getOwnerUid())) {
+                    throw new IllegalStateException("The owner UID must be included in "
+                            + " administrator UIDs.");
+                }
+            }
+            return new NetworkCapabilities(mCaps);
+        }
+    }
+}
diff --git a/framework/src/android/net/NetworkConfig.java b/framework/src/android/net/NetworkConfig.java
new file mode 100644
index 0000000..32a2cda
--- /dev/null
+++ b/framework/src/android/net/NetworkConfig.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+import java.util.Locale;
+
+/**
+ * Describes the buildtime configuration of a network.
+ * Holds settings read from resources.
+ * @hide
+ */
+public class NetworkConfig {
+    /**
+     * Human readable string
+     */
+    public String name;
+
+    /**
+     * Type from ConnectivityManager
+     */
+    public int type;
+
+    /**
+     * the radio number from radio attributes config
+     */
+    public int radio;
+
+    /**
+     * higher number == higher priority when turning off connections
+     */
+    public int priority;
+
+    /**
+     * indicates the boot time dependencyMet setting
+     */
+    public boolean dependencyMet;
+
+    /**
+     * indicates the default restoral timer in seconds
+     * if the network is used as a special network feature
+     * -1 indicates no restoration of default
+     */
+    public int restoreTime;
+
+    /**
+     * input string from config.xml resource.  Uses the form:
+     * [Connection name],[ConnectivityManager connection type],
+     * [associated radio-type],[priority],[dependencyMet]
+     */
+    public NetworkConfig(String init) {
+        String fragments[] = init.split(",");
+        name = fragments[0].trim().toLowerCase(Locale.ROOT);
+        type = Integer.parseInt(fragments[1]);
+        radio = Integer.parseInt(fragments[2]);
+        priority = Integer.parseInt(fragments[3]);
+        restoreTime = Integer.parseInt(fragments[4]);
+        dependencyMet = Boolean.parseBoolean(fragments[5]);
+    }
+
+    /**
+     * Indicates if this network is supposed to be default-routable
+     */
+    public boolean isDefault() {
+        return (type == radio);
+    }
+}
diff --git a/framework/src/android/net/NetworkInfo.java b/framework/src/android/net/NetworkInfo.java
new file mode 100644
index 0000000..bb23494
--- /dev/null
+++ b/framework/src/android/net/NetworkInfo.java
@@ -0,0 +1,625 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.EnumMap;
+
+/**
+ * Describes the status of a network interface.
+ * <p>Use {@link ConnectivityManager#getActiveNetworkInfo()} to get an instance that represents
+ * the current network connection.
+ *
+ * @deprecated Callers should instead use the {@link ConnectivityManager.NetworkCallback} API to
+ *             learn about connectivity changes, or switch to use
+ *             {@link ConnectivityManager#getNetworkCapabilities} or
+ *             {@link ConnectivityManager#getLinkProperties} to get information synchronously. Keep
+ *             in mind that while callbacks are guaranteed to be called for every event in order,
+ *             synchronous calls have no such constraints, and as such it is unadvisable to use the
+ *             synchronous methods inside the callbacks as they will often not offer a view of
+ *             networking that is consistent (that is: they may return a past or a future state with
+ *             respect to the event being processed by the callback). Instead, callers are advised
+ *             to only use the arguments of the callbacks, possibly memorizing the specific bits of
+ *             information they need to keep from one callback to another.
+ */
+@Deprecated
+public class NetworkInfo implements Parcelable {
+
+    /**
+     * Coarse-grained network state. This is probably what most applications should
+     * use, rather than {@link android.net.NetworkInfo.DetailedState DetailedState}.
+     * The mapping between the two is as follows:
+     * <br/><br/>
+     * <table>
+     * <tr><td><b>Detailed state</b></td><td><b>Coarse-grained state</b></td></tr>
+     * <tr><td><code>IDLE</code></td><td><code>DISCONNECTED</code></td></tr>
+     * <tr><td><code>SCANNING</code></td><td><code>DISCONNECTED</code></td></tr>
+     * <tr><td><code>CONNECTING</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>AUTHENTICATING</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>OBTAINING_IPADDR</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>VERIFYING_POOR_LINK</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>CAPTIVE_PORTAL_CHECK</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>CONNECTED</code></td><td><code>CONNECTED</code></td></tr>
+     * <tr><td><code>SUSPENDED</code></td><td><code>SUSPENDED</code></td></tr>
+     * <tr><td><code>DISCONNECTING</code></td><td><code>DISCONNECTING</code></td></tr>
+     * <tr><td><code>DISCONNECTED</code></td><td><code>DISCONNECTED</code></td></tr>
+     * <tr><td><code>FAILED</code></td><td><code>DISCONNECTED</code></td></tr>
+     * <tr><td><code>BLOCKED</code></td><td><code>DISCONNECTED</code></td></tr>
+     * </table>
+     *
+     * @deprecated See {@link NetworkInfo}.
+     */
+    @Deprecated
+    public enum State {
+        CONNECTING, CONNECTED, SUSPENDED, DISCONNECTING, DISCONNECTED, UNKNOWN
+    }
+
+    /**
+     * The fine-grained state of a network connection. This level of detail
+     * is probably of interest to few applications. Most should use
+     * {@link android.net.NetworkInfo.State State} instead.
+     *
+     * @deprecated See {@link NetworkInfo}.
+     */
+    @Deprecated
+    public enum DetailedState {
+        /** Ready to start data connection setup. */
+        IDLE,
+        /** Searching for an available access point. */
+        SCANNING,
+        /** Currently setting up data connection. */
+        CONNECTING,
+        /** Network link established, performing authentication. */
+        AUTHENTICATING,
+        /** Awaiting response from DHCP server in order to assign IP address information. */
+        OBTAINING_IPADDR,
+        /** IP traffic should be available. */
+        CONNECTED,
+        /** IP traffic is suspended */
+        SUSPENDED,
+        /** Currently tearing down data connection. */
+        DISCONNECTING,
+        /** IP traffic not available. */
+        DISCONNECTED,
+        /** Attempt to connect failed. */
+        FAILED,
+        /** Access to this network is blocked. */
+        BLOCKED,
+        /** Link has poor connectivity. */
+        VERIFYING_POOR_LINK,
+        /** Checking if network is a captive portal */
+        CAPTIVE_PORTAL_CHECK
+    }
+
+    /**
+     * This is the map described in the Javadoc comment above. The positions
+     * of the elements of the array must correspond to the ordinal values
+     * of <code>DetailedState</code>.
+     */
+    private static final EnumMap<DetailedState, State> stateMap =
+        new EnumMap<DetailedState, State>(DetailedState.class);
+
+    static {
+        stateMap.put(DetailedState.IDLE, State.DISCONNECTED);
+        stateMap.put(DetailedState.SCANNING, State.DISCONNECTED);
+        stateMap.put(DetailedState.CONNECTING, State.CONNECTING);
+        stateMap.put(DetailedState.AUTHENTICATING, State.CONNECTING);
+        stateMap.put(DetailedState.OBTAINING_IPADDR, State.CONNECTING);
+        stateMap.put(DetailedState.VERIFYING_POOR_LINK, State.CONNECTING);
+        stateMap.put(DetailedState.CAPTIVE_PORTAL_CHECK, State.CONNECTING);
+        stateMap.put(DetailedState.CONNECTED, State.CONNECTED);
+        stateMap.put(DetailedState.SUSPENDED, State.SUSPENDED);
+        stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING);
+        stateMap.put(DetailedState.DISCONNECTED, State.DISCONNECTED);
+        stateMap.put(DetailedState.FAILED, State.DISCONNECTED);
+        stateMap.put(DetailedState.BLOCKED, State.DISCONNECTED);
+    }
+
+    private int mNetworkType;
+    private int mSubtype;
+    private String mTypeName;
+    private String mSubtypeName;
+    @NonNull
+    private State mState;
+    @NonNull
+    private DetailedState mDetailedState;
+    private String mReason;
+    private String mExtraInfo;
+    private boolean mIsFailover;
+    private boolean mIsAvailable;
+    private boolean mIsRoaming;
+
+    /**
+     * Create a new instance of NetworkInfo.
+     *
+     * This may be useful for apps to write unit tests.
+     *
+     * @param type the legacy type of the network, as one of the ConnectivityManager.TYPE_*
+     *             constants.
+     * @param subtype the subtype if applicable, as one of the TelephonyManager.NETWORK_TYPE_*
+     *                constants.
+     * @param typeName a human-readable string for the network type, or an empty string or null.
+     * @param subtypeName a human-readable string for the subtype, or an empty string or null.
+     */
+    public NetworkInfo(int type, int subtype,
+            @Nullable String typeName, @Nullable String subtypeName) {
+        if (!ConnectivityManager.isNetworkTypeValid(type)
+                && type != ConnectivityManager.TYPE_NONE) {
+            throw new IllegalArgumentException("Invalid network type: " + type);
+        }
+        mNetworkType = type;
+        mSubtype = subtype;
+        mTypeName = typeName;
+        mSubtypeName = subtypeName;
+        setDetailedState(DetailedState.IDLE, null, null);
+        mState = State.UNKNOWN;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public NetworkInfo(NetworkInfo source) {
+        if (source != null) {
+            synchronized (source) {
+                mNetworkType = source.mNetworkType;
+                mSubtype = source.mSubtype;
+                mTypeName = source.mTypeName;
+                mSubtypeName = source.mSubtypeName;
+                mState = source.mState;
+                mDetailedState = source.mDetailedState;
+                mReason = source.mReason;
+                mExtraInfo = source.mExtraInfo;
+                mIsFailover = source.mIsFailover;
+                mIsAvailable = source.mIsAvailable;
+                mIsRoaming = source.mIsRoaming;
+            }
+        }
+    }
+
+    /**
+     * Reports the type of network to which the
+     * info in this {@code NetworkInfo} pertains.
+     * @return one of {@link ConnectivityManager#TYPE_MOBILE}, {@link
+     * ConnectivityManager#TYPE_WIFI}, {@link ConnectivityManager#TYPE_WIMAX}, {@link
+     * ConnectivityManager#TYPE_ETHERNET},  {@link ConnectivityManager#TYPE_BLUETOOTH}, or other
+     * types defined by {@link ConnectivityManager}.
+     * @deprecated Callers should switch to checking {@link NetworkCapabilities#hasTransport}
+     *             instead with one of the NetworkCapabilities#TRANSPORT_* constants :
+     *             {@link #getType} and {@link #getTypeName} cannot account for networks using
+     *             multiple transports. Note that generally apps should not care about transport;
+     *             {@link NetworkCapabilities#NET_CAPABILITY_NOT_METERED} and
+     *             {@link NetworkCapabilities#getLinkDownstreamBandwidthKbps} are calls that
+     *             apps concerned with meteredness or bandwidth should be looking at, as they
+     *             offer this information with much better accuracy.
+     */
+    @Deprecated
+    public int getType() {
+        synchronized (this) {
+            return mNetworkType;
+        }
+    }
+
+    /**
+     * @deprecated Use {@link NetworkCapabilities} instead
+     * @hide
+     */
+    @Deprecated
+    public void setType(int type) {
+        synchronized (this) {
+            mNetworkType = type;
+        }
+    }
+
+    /**
+     * Return a network-type-specific integer describing the subtype
+     * of the network.
+     * @return the network subtype
+     * @deprecated Use {@link android.telephony.TelephonyManager#getDataNetworkType} instead.
+     */
+    @Deprecated
+    public int getSubtype() {
+        synchronized (this) {
+            return mSubtype;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setSubtype(int subtype, String subtypeName) {
+        synchronized (this) {
+            mSubtype = subtype;
+            mSubtypeName = subtypeName;
+        }
+    }
+
+    /**
+     * Return a human-readable name describe the type of the network,
+     * for example "WIFI" or "MOBILE".
+     * @return the name of the network type
+     * @deprecated Callers should switch to checking {@link NetworkCapabilities#hasTransport}
+     *             instead with one of the NetworkCapabilities#TRANSPORT_* constants :
+     *             {@link #getType} and {@link #getTypeName} cannot account for networks using
+     *             multiple transports. Note that generally apps should not care about transport;
+     *             {@link NetworkCapabilities#NET_CAPABILITY_NOT_METERED} and
+     *             {@link NetworkCapabilities#getLinkDownstreamBandwidthKbps} are calls that
+     *             apps concerned with meteredness or bandwidth should be looking at, as they
+     *             offer this information with much better accuracy.
+     */
+    @Deprecated
+    public String getTypeName() {
+        synchronized (this) {
+            return mTypeName;
+        }
+    }
+
+    /**
+     * Return a human-readable name describing the subtype of the network.
+     * @return the name of the network subtype
+     * @deprecated Use {@link android.telephony.TelephonyManager#getDataNetworkType} instead.
+     */
+    @Deprecated
+    public String getSubtypeName() {
+        synchronized (this) {
+            return mSubtypeName;
+        }
+    }
+
+    /**
+     * Indicates whether network connectivity exists or is in the process
+     * of being established. This is good for applications that need to
+     * do anything related to the network other than read or write data.
+     * For the latter, call {@link #isConnected()} instead, which guarantees
+     * that the network is fully usable.
+     * @return {@code true} if network connectivity exists or is in the process
+     * of being established, {@code false} otherwise.
+     * @deprecated Apps should instead use the
+     *             {@link android.net.ConnectivityManager.NetworkCallback} API to
+     *             learn about connectivity changes.
+     *             {@link ConnectivityManager#registerDefaultNetworkCallback} and
+     *             {@link ConnectivityManager#registerNetworkCallback}. These will
+     *             give a more accurate picture of the connectivity state of
+     *             the device and let apps react more easily and quickly to changes.
+     */
+    @Deprecated
+    public boolean isConnectedOrConnecting() {
+        synchronized (this) {
+            return mState == State.CONNECTED || mState == State.CONNECTING;
+        }
+    }
+
+    /**
+     * Indicates whether network connectivity exists and it is possible to establish
+     * connections and pass data.
+     * <p>Always call this before attempting to perform data transactions.
+     * @return {@code true} if network connectivity exists, {@code false} otherwise.
+     * @deprecated Apps should instead use the
+     *             {@link android.net.ConnectivityManager.NetworkCallback} API to
+     *             learn about connectivity changes. See
+     *             {@link ConnectivityManager#registerDefaultNetworkCallback} and
+     *             {@link ConnectivityManager#registerNetworkCallback}. These will
+     *             give a more accurate picture of the connectivity state of
+     *             the device and let apps react more easily and quickly to changes.
+     */
+    @Deprecated
+    public boolean isConnected() {
+        synchronized (this) {
+            return mState == State.CONNECTED;
+        }
+    }
+
+    /**
+     * Indicates whether network connectivity is possible. A network is unavailable
+     * when a persistent or semi-persistent condition prevents the possibility
+     * of connecting to that network. Examples include
+     * <ul>
+     * <li>The device is out of the coverage area for any network of this type.</li>
+     * <li>The device is on a network other than the home network (i.e., roaming), and
+     * data roaming has been disabled.</li>
+     * <li>The device's radio is turned off, e.g., because airplane mode is enabled.</li>
+     * </ul>
+     * Since Android L, this always returns {@code true}, because the system only
+     * returns info for available networks.
+     * @return {@code true} if the network is available, {@code false} otherwise
+     * @deprecated Apps should instead use the
+     *             {@link android.net.ConnectivityManager.NetworkCallback} API to
+     *             learn about connectivity changes.
+     *             {@link ConnectivityManager#registerDefaultNetworkCallback} and
+     *             {@link ConnectivityManager#registerNetworkCallback}. These will
+     *             give a more accurate picture of the connectivity state of
+     *             the device and let apps react more easily and quickly to changes.
+     */
+    @Deprecated
+    public boolean isAvailable() {
+        synchronized (this) {
+            return mIsAvailable;
+        }
+    }
+
+    /**
+     * Sets if the network is available, ie, if the connectivity is possible.
+     * @param isAvailable the new availability value.
+     * @deprecated Use {@link NetworkCapabilities} instead
+     *
+     * @hide
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public void setIsAvailable(boolean isAvailable) {
+        synchronized (this) {
+            mIsAvailable = isAvailable;
+        }
+    }
+
+    /**
+     * Indicates whether the current attempt to connect to the network
+     * resulted from the ConnectivityManager trying to fail over to this
+     * network following a disconnect from another network.
+     * @return {@code true} if this is a failover attempt, {@code false}
+     * otherwise.
+     * @deprecated This field is not populated in recent Android releases,
+     *             and does not make a lot of sense in a multi-network world.
+     */
+    @Deprecated
+    public boolean isFailover() {
+        synchronized (this) {
+            return mIsFailover;
+        }
+    }
+
+    /**
+     * Set the failover boolean.
+     * @param isFailover {@code true} to mark the current connection attempt
+     * as a failover.
+     * @deprecated This hasn't been set in any recent Android release.
+     * @hide
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public void setFailover(boolean isFailover) {
+        synchronized (this) {
+            mIsFailover = isFailover;
+        }
+    }
+
+    /**
+     * Indicates whether the device is currently roaming on this network. When
+     * {@code true}, it suggests that use of data on this network may incur
+     * extra costs.
+     *
+     * @return {@code true} if roaming is in effect, {@code false} otherwise.
+     * @deprecated Callers should switch to checking
+     *             {@link NetworkCapabilities#NET_CAPABILITY_NOT_ROAMING}
+     *             instead, since that handles more complex situations, such as
+     *             VPNs.
+     */
+    @Deprecated
+    public boolean isRoaming() {
+        synchronized (this) {
+            return mIsRoaming;
+        }
+    }
+
+    /**
+     * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_NOT_ROAMING} instead.
+     * {@hide}
+     */
+    @VisibleForTesting
+    @Deprecated
+    @UnsupportedAppUsage
+    public void setRoaming(boolean isRoaming) {
+        synchronized (this) {
+            mIsRoaming = isRoaming;
+        }
+    }
+
+    /**
+     * Reports the current coarse-grained state of the network.
+     * @return the coarse-grained state
+     * @deprecated Apps should instead use the
+     *             {@link android.net.ConnectivityManager.NetworkCallback} API to
+     *             learn about connectivity changes.
+     *             {@link ConnectivityManager#registerDefaultNetworkCallback} and
+     *             {@link ConnectivityManager#registerNetworkCallback}. These will
+     *             give a more accurate picture of the connectivity state of
+     *             the device and let apps react more easily and quickly to changes.
+     */
+    @Deprecated
+    public State getState() {
+        synchronized (this) {
+            return mState;
+        }
+    }
+
+    /**
+     * Reports the current fine-grained state of the network.
+     * @return the fine-grained state
+     * @deprecated Apps should instead use the
+     *             {@link android.net.ConnectivityManager.NetworkCallback} API to
+     *             learn about connectivity changes. See
+     *             {@link ConnectivityManager#registerDefaultNetworkCallback} and
+     *             {@link ConnectivityManager#registerNetworkCallback}. These will
+     *             give a more accurate picture of the connectivity state of
+     *             the device and let apps react more easily and quickly to changes.
+     */
+    @Deprecated
+    public @NonNull DetailedState getDetailedState() {
+        synchronized (this) {
+            return mDetailedState;
+        }
+    }
+
+    /**
+     * Sets the fine-grained state of the network.
+     *
+     * This is only useful for testing.
+     *
+     * @param detailedState the {@link DetailedState}.
+     * @param reason a {@code String} indicating the reason for the state change,
+     * if one was supplied. May be {@code null}.
+     * @param extraInfo an optional {@code String} providing addditional network state
+     * information passed up from the lower networking layers.
+     * @deprecated Use {@link NetworkCapabilities} instead.
+     */
+    @Deprecated
+    public void setDetailedState(@NonNull DetailedState detailedState, @Nullable String reason,
+            @Nullable String extraInfo) {
+        synchronized (this) {
+            this.mDetailedState = detailedState;
+            this.mState = stateMap.get(detailedState);
+            this.mReason = reason;
+            this.mExtraInfo = extraInfo;
+        }
+    }
+
+    /**
+     * Set the extraInfo field.
+     * @param extraInfo an optional {@code String} providing addditional network state
+     * information passed up from the lower networking layers.
+     * @deprecated See {@link NetworkInfo#getExtraInfo}.
+     * @hide
+     */
+    @Deprecated
+    public void setExtraInfo(String extraInfo) {
+        synchronized (this) {
+            this.mExtraInfo = extraInfo;
+        }
+    }
+
+    /**
+     * Report the reason an attempt to establish connectivity failed,
+     * if one is available.
+     * @return the reason for failure, or null if not available
+     * @deprecated This method does not have a consistent contract that could make it useful
+     *             to callers.
+     */
+    public String getReason() {
+        synchronized (this) {
+            return mReason;
+        }
+    }
+
+    /**
+     * Report the extra information about the network state, if any was
+     * provided by the lower networking layers.
+     * @return the extra information, or null if not available
+     * @deprecated Use other services e.g. WifiManager to get additional information passed up from
+     *             the lower networking layers.
+     */
+    @Deprecated
+    public String getExtraInfo() {
+        synchronized (this) {
+            return mExtraInfo;
+        }
+    }
+
+    @Override
+    public String toString() {
+        synchronized (this) {
+            final StringBuilder builder = new StringBuilder("[");
+            builder.append("type: ").append(getTypeName()).append("[").append(getSubtypeName()).
+            append("], state: ").append(mState).append("/").append(mDetailedState).
+            append(", reason: ").append(mReason == null ? "(unspecified)" : mReason).
+            append(", extra: ").append(mExtraInfo == null ? "(none)" : mExtraInfo).
+            append(", failover: ").append(mIsFailover).
+            append(", available: ").append(mIsAvailable).
+            append(", roaming: ").append(mIsRoaming).
+            append("]");
+            return builder.toString();
+        }
+    }
+
+    /**
+     * Returns a brief summary string suitable for debugging.
+     * @hide
+     */
+    public String toShortString() {
+        synchronized (this) {
+            final StringBuilder builder = new StringBuilder();
+            builder.append(getTypeName());
+
+            final String subtype = getSubtypeName();
+            if (!TextUtils.isEmpty(subtype)) {
+                builder.append("[").append(subtype).append("]");
+            }
+
+            builder.append(" ");
+            builder.append(mDetailedState);
+            if (mIsRoaming) {
+                builder.append(" ROAMING");
+            }
+            if (mExtraInfo != null) {
+                builder.append(" extra: ").append(mExtraInfo);
+            }
+            return builder.toString();
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        synchronized (this) {
+            dest.writeInt(mNetworkType);
+            dest.writeInt(mSubtype);
+            dest.writeString(mTypeName);
+            dest.writeString(mSubtypeName);
+            dest.writeString(mState.name());
+            dest.writeString(mDetailedState.name());
+            dest.writeInt(mIsFailover ? 1 : 0);
+            dest.writeInt(mIsAvailable ? 1 : 0);
+            dest.writeInt(mIsRoaming ? 1 : 0);
+            dest.writeString(mReason);
+            dest.writeString(mExtraInfo);
+        }
+    }
+
+    public static final @android.annotation.NonNull Creator<NetworkInfo> CREATOR = new Creator<NetworkInfo>() {
+        @Override
+        public NetworkInfo createFromParcel(Parcel in) {
+            int netType = in.readInt();
+            int subtype = in.readInt();
+            String typeName = in.readString();
+            String subtypeName = in.readString();
+            NetworkInfo netInfo = new NetworkInfo(netType, subtype, typeName, subtypeName);
+            netInfo.mState = State.valueOf(in.readString());
+            netInfo.mDetailedState = DetailedState.valueOf(in.readString());
+            netInfo.mIsFailover = in.readInt() != 0;
+            netInfo.mIsAvailable = in.readInt() != 0;
+            netInfo.mIsRoaming = in.readInt() != 0;
+            netInfo.mReason = in.readString();
+            netInfo.mExtraInfo = in.readString();
+            return netInfo;
+        }
+
+        @Override
+        public NetworkInfo[] newArray(int size) {
+            return new NetworkInfo[size];
+        }
+    };
+}
diff --git a/framework/src/android/net/NetworkProvider.java b/framework/src/android/net/NetworkProvider.java
new file mode 100644
index 0000000..0665af5
--- /dev/null
+++ b/framework/src/android/net/NetworkProvider.java
@@ -0,0 +1,332 @@
+/*
+ * 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;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+
+/**
+ * Base class for network providers such as telephony or Wi-Fi. NetworkProviders connect the device
+ * to networks and makes them available to the core network stack by creating
+ * {@link NetworkAgent}s. The networks can then provide connectivity to apps and can be interacted
+ * with via networking APIs such as {@link ConnectivityManager}.
+ *
+ * Subclasses should implement {@link #onNetworkRequested} and {@link #onNetworkRequestWithdrawn}
+ * to receive {@link NetworkRequest}s sent by the system and by apps. A network that is not the
+ * best (highest-scoring) network for any request is generally not used by the system, and torn
+ * down.
+ *
+ * @hide
+ */
+@SystemApi
+public class NetworkProvider {
+    /**
+     * {@code providerId} value that indicates the absence of a provider. It is the providerId of
+     * any NetworkProvider that is not currently registered, and of any NetworkRequest that is not
+     * currently being satisfied by a network.
+     */
+    public static final int ID_NONE = -1;
+
+    /**
+     * The first providerId value that will be allocated.
+     * @hide only used by ConnectivityService.
+     */
+    public static final int FIRST_PROVIDER_ID = 1;
+
+    /** @hide only used by ConnectivityService */
+    public static final int CMD_REQUEST_NETWORK = 1;
+    /** @hide only used by ConnectivityService */
+    public static final int CMD_CANCEL_REQUEST = 2;
+
+    private final Messenger mMessenger;
+    private final String mName;
+    private final Context mContext;
+
+    private int mProviderId = ID_NONE;
+
+    /**
+     * Constructs a new NetworkProvider.
+     *
+     * @param looper the Looper on which to run {@link #onNetworkRequested} and
+     *               {@link #onNetworkRequestWithdrawn}.
+     * @param name the name of the listener, used only for debugging.
+     *
+     * @hide
+     */
+    @SystemApi
+    public NetworkProvider(@NonNull Context context, @NonNull Looper looper, @NonNull String name) {
+        // TODO (b/174636568) : this class should be able to cache an instance of
+        // ConnectivityManager so it doesn't have to fetch it again every time.
+        final Handler handler = new Handler(looper) {
+            @Override
+            public void handleMessage(Message m) {
+                switch (m.what) {
+                    case CMD_REQUEST_NETWORK:
+                        onNetworkRequested((NetworkRequest) m.obj, m.arg1, m.arg2);
+                        break;
+                    case CMD_CANCEL_REQUEST:
+                        onNetworkRequestWithdrawn((NetworkRequest) m.obj);
+                        break;
+                    default:
+                        Log.e(mName, "Unhandled message: " + m.what);
+                }
+            }
+        };
+        mContext = context;
+        mMessenger = new Messenger(handler);
+        mName = name;
+    }
+
+    // TODO: consider adding a register() method so ConnectivityManager does not need to call this.
+    /** @hide */
+    public @Nullable Messenger getMessenger() {
+        return mMessenger;
+    }
+
+    /** @hide */
+    public @NonNull String getName() {
+        return mName;
+    }
+
+    /**
+     * Returns the ID of this provider. This is known only once the provider is registered via
+     * {@link ConnectivityManager#registerNetworkProvider()}, otherwise the ID is {@link #ID_NONE}.
+     * This ID must be used when registering any {@link NetworkAgent}s.
+     */
+    public int getProviderId() {
+        return mProviderId;
+    }
+
+    /** @hide */
+    public void setProviderId(int providerId) {
+        mProviderId = providerId;
+    }
+
+    /**
+     *  Called when a NetworkRequest is received. The request may be a new request or an existing
+     *  request with a different score.
+     *
+     * @param request the NetworkRequest being received
+     * @param score the score of the network currently satisfying the request, or 0 if none.
+     * @param providerId the ID of the provider that created the network currently satisfying this
+     *                   request, or {@link #ID_NONE} if none.
+     *
+     *  @hide
+     */
+    @SystemApi
+    public void onNetworkRequested(@NonNull NetworkRequest request,
+            @IntRange(from = 0, to = 99) int score, int providerId) {}
+
+    /**
+     *  Called when a NetworkRequest is withdrawn.
+     *  @hide
+     */
+    @SystemApi
+    public void onNetworkRequestWithdrawn(@NonNull NetworkRequest request) {}
+
+    /**
+     * Asserts that no provider will ever be able to satisfy the specified request. The provider
+     * must only call this method if it knows that it is the only provider on the system capable of
+     * satisfying this request, and that the request cannot be satisfied. The application filing the
+     * request will receive an {@link NetworkCallback#onUnavailable()} callback.
+     *
+     * @param request the request that permanently cannot be fulfilled
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
+    public void declareNetworkRequestUnfulfillable(@NonNull NetworkRequest request) {
+        ConnectivityManager.from(mContext).declareNetworkRequestUnfulfillable(request);
+    }
+
+    /**
+     * A callback for parties registering a NetworkOffer.
+     *
+     * This is used with {@link ConnectivityManager#offerNetwork}. When offering a network,
+     * the system will use this callback to inform the caller that a network corresponding to
+     * this offer is needed or unneeded.
+     *
+     * @hide
+     */
+    @SystemApi
+    public interface NetworkOfferCallback {
+        /**
+         * Called by the system when a network for this offer is needed to satisfy some
+         * networking request.
+         */
+        void onNetworkNeeded(@NonNull NetworkRequest request);
+        /**
+         * Called by the system when this offer is no longer valuable for this request.
+         */
+        void onNetworkUnneeded(@NonNull NetworkRequest request);
+    }
+
+    private class NetworkOfferCallbackProxy extends INetworkOfferCallback.Stub {
+        @NonNull public final NetworkOfferCallback callback;
+        @NonNull private final Executor mExecutor;
+
+        NetworkOfferCallbackProxy(@NonNull final NetworkOfferCallback callback,
+                @NonNull final Executor executor) {
+            this.callback = callback;
+            this.mExecutor = executor;
+        }
+
+        @Override
+        public void onNetworkNeeded(final @NonNull NetworkRequest request) {
+            mExecutor.execute(() -> callback.onNetworkNeeded(request));
+        }
+
+        @Override
+        public void onNetworkUnneeded(final @NonNull NetworkRequest request) {
+            mExecutor.execute(() -> callback.onNetworkUnneeded(request));
+        }
+    }
+
+    @GuardedBy("mProxies")
+    @NonNull private final ArrayList<NetworkOfferCallbackProxy> mProxies = new ArrayList<>();
+
+    // Returns the proxy associated with this callback, or null if none.
+    @Nullable
+    private NetworkOfferCallbackProxy findProxyForCallback(@NonNull final NetworkOfferCallback cb) {
+        synchronized (mProxies) {
+            for (final NetworkOfferCallbackProxy p : mProxies) {
+                if (p.callback == cb) return p;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Register or update an offer for network with the passed capabilities and score.
+     *
+     * A NetworkProvider's role is to provide networks. This method is how a provider tells the
+     * connectivity stack what kind of network it may provide. The score and caps arguments act
+     * as filters that the connectivity stack uses to tell when the offer is valuable. When an
+     * offer might be preferred over existing networks, the provider will receive a call to
+     * the associated callback's {@link NetworkOfferCallback#onNetworkNeeded} method. The provider
+     * should then try to bring up this network. When an offer is no longer useful, the stack
+     * will inform the provider by calling {@link NetworkOfferCallback#onNetworkUnneeded}. The
+     * provider should stop trying to bring up such a network, or disconnect it if it already has
+     * one.
+     *
+     * The stack determines what offers are valuable according to what networks are currently
+     * available to the system, and what networking requests are made by applications. If an
+     * offer looks like it could connect a better network than any existing network for any
+     * particular request, that's when the stack decides the network is needed. If the current
+     * networking requests are all satisfied by networks that this offer couldn't possibly be a
+     * better match for, that's when the offer is no longer valuable. An offer starts out as
+     * unneeded ; the provider should not try to bring up the network until
+     * {@link NetworkOfferCallback#onNetworkNeeded} is called.
+     *
+     * Note that the offers are non-binding to the providers, in particular because providers
+     * often don't know if they will be able to bring up such a network at any given time. For
+     * example, no wireless network may be in range when the offer would be valuable. This is fine
+     * and expected ; the provider should simply continue to try to bring up the network and do so
+     * if/when it becomes possible. In the mean time, the stack will continue to satisfy requests
+     * with the best network currently available, or if none, keep the apps informed that no
+     * network can currently satisfy this request. When/if the provider can bring up the network,
+     * the connectivity stack will match it against requests, and inform interested apps of the
+     * availability of this network. This may, in turn, render the offer of some other provider
+     * low-value if all requests it used to satisfy are now better served by this network.
+     *
+     * A network can become unneeded for a reason like the above : whether the provider managed
+     * to bring up the offered network after it became needed or not, some other provider may
+     * bring up a better network than this one, making this network unneeded. A network may also
+     * become unneeded if the application making the request withdrew it (for example, after it
+     * is done transferring data, or if the user canceled an operation).
+     *
+     * The capabilities and score act as filters as to what requests the provider will see.
+     * They are not promises, but for best performance, the providers should strive to put
+     * as much known information as possible in the offer. For the score, it should put as
+     * strong a score as the networks will have, since this will filter what requests the
+     * provider sees – it's not a promise, it only serves to avoid sending requests that
+     * the provider can't ever hope to satisfy better than any current network. For capabilities,
+     * it should put all NetworkAgent-managed capabilities a network may have, even if it doesn't
+     * have them at first. This applies to INTERNET, for example ; if a provider thinks the
+     * network it can bring up for this offer may offer Internet access it should include the
+     * INTERNET bit. It's fine if the brought up network ends up not actually having INTERNET.
+     *
+     * TODO : in the future, to avoid possible infinite loops, there should be constraints on
+     * what can be put in capabilities of networks brought up for an offer. If a provider might
+     * bring up a network with or without INTERNET, then it should file two offers : this will
+     * let it know precisely what networks are needed, so it can avoid bringing up networks that
+     * won't actually satisfy requests and remove the risk for bring-up-bring-down loops.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
+    public void registerNetworkOffer(@NonNull final NetworkScore score,
+            @NonNull final NetworkCapabilities caps, @NonNull final Executor executor,
+            @NonNull final NetworkOfferCallback callback) {
+        // Can't offer a network with a provider that is not yet registered or already unregistered.
+        final int providerId = mProviderId;
+        if (providerId == ID_NONE) return;
+        NetworkOfferCallbackProxy proxy = null;
+        synchronized (mProxies) {
+            for (final NetworkOfferCallbackProxy existingProxy : mProxies) {
+                if (existingProxy.callback == callback) {
+                    proxy = existingProxy;
+                    break;
+                }
+            }
+            if (null == proxy) {
+                proxy = new NetworkOfferCallbackProxy(callback, executor);
+                mProxies.add(proxy);
+            }
+        }
+        mContext.getSystemService(ConnectivityManager.class)
+                .offerNetwork(providerId, score, caps, proxy);
+    }
+
+    /**
+     * Withdraw a network offer previously made to the networking stack.
+     *
+     * If a provider can no longer provide a network they offered, it should call this method.
+     * An example of usage could be if the hardware necessary to bring up the network was turned
+     * off in UI by the user. Note that because offers are never binding, the provider might
+     * alternatively decide not to withdraw this offer and simply refuse to bring up the network
+     * even when it's needed. However, withdrawing the request is slightly more resource-efficient
+     * because the networking stack won't have to compare this offer to exiting networks to see
+     * if it could beat any of them, and may be advantageous to the provider's implementation that
+     * can rely on no longer receiving callbacks for a network that they can't bring up anyways.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
+    public void unregisterNetworkOffer(final @NonNull NetworkOfferCallback callback) {
+        final NetworkOfferCallbackProxy proxy = findProxyForCallback(callback);
+        if (null == proxy) return;
+        mProxies.remove(proxy);
+        mContext.getSystemService(ConnectivityManager.class).unofferNetwork(proxy);
+    }
+}
diff --git a/framework/src/android/net/NetworkReleasedException.java b/framework/src/android/net/NetworkReleasedException.java
new file mode 100644
index 0000000..0629b75
--- /dev/null
+++ b/framework/src/android/net/NetworkReleasedException.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * Indicates that the {@link Network} was released and is no longer available.
+ *
+ * @hide
+ */
+@SystemApi
+public class NetworkReleasedException extends Exception {
+    /** @hide */
+    public NetworkReleasedException() {
+        super("The network was released and is no longer available");
+    }
+}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
new file mode 100644
index 0000000..e6a96ef
--- /dev/null
+++ b/framework/src/android/net/NetworkRequest.java
@@ -0,0 +1,743 @@
+/*
+ * 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.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.net.NetworkCapabilities.NetCapability;
+import android.net.NetworkCapabilities.Transport;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Range;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Defines a request for a network, made through {@link NetworkRequest.Builder} and used
+ * to request a network via {@link ConnectivityManager#requestNetwork} or listen for changes
+ * via {@link ConnectivityManager#registerNetworkCallback}.
+ */
+public class NetworkRequest implements Parcelable {
+    /**
+     * The first requestId value that will be allocated.
+     * @hide only used by ConnectivityService.
+     */
+    public static final int FIRST_REQUEST_ID = 1;
+
+    /**
+     * The requestId value that represents the absence of a request.
+     * @hide only used by ConnectivityService.
+     */
+    public static final int REQUEST_ID_NONE = -1;
+
+    /**
+     * The {@link NetworkCapabilities} that define this request.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public final @NonNull NetworkCapabilities networkCapabilities;
+
+    /**
+     * Identifies the request.  NetworkRequests should only be constructed by
+     * the Framework and given out to applications as tokens to be used to identify
+     * the request.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public final int requestId;
+
+    /**
+     * Set for legacy requests and the default.  Set to TYPE_NONE for none.
+     * Causes CONNECTIVITY_ACTION broadcasts to be sent.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public final int legacyType;
+
+    /**
+     * A NetworkRequest as used by the system can be one of the following types:
+     *
+     *     - LISTEN, for which the framework will issue callbacks about any
+     *       and all networks that match the specified NetworkCapabilities,
+     *
+     *     - REQUEST, capable of causing a specific network to be created
+     *       first (e.g. a telephony DUN request), the framework will issue
+     *       callbacks about the single, highest scoring current network
+     *       (if any) that matches the specified NetworkCapabilities, or
+     *
+     *     - TRACK_DEFAULT, which causes the framework to issue callbacks for
+     *       the single, highest scoring current network (if any) that will
+     *       be chosen for an app, but which cannot cause the framework to
+     *       either create or retain the existence of any specific network.
+     *
+     *     - TRACK_SYSTEM_DEFAULT, which causes the framework to send callbacks
+     *       for the network (if any) that satisfies the default Internet
+     *       request.
+     *
+     *     - TRACK_BEST, which causes the framework to send callbacks about
+     *       the single, highest scoring current network (if any) that matches
+     *       the specified NetworkCapabilities.
+     *
+     *     - BACKGROUND_REQUEST, like REQUEST but does not cause any networks
+     *       to retain the NET_CAPABILITY_FOREGROUND capability. A network with
+     *       no foreground requests is in the background. A network that has
+     *       one or more background requests and loses its last foreground
+     *       request to a higher-scoring network will not go into the
+     *       background immediately, but will linger and go into the background
+     *       after the linger timeout.
+     *
+     *     - The value NONE is used only by applications. When an application
+     *       creates a NetworkRequest, it does not have a type; the type is set
+     *       by the system depending on the method used to file the request
+     *       (requestNetwork, registerNetworkCallback, etc.).
+     *
+     * @hide
+     */
+    public static enum Type {
+        NONE,
+        LISTEN,
+        TRACK_DEFAULT,
+        REQUEST,
+        BACKGROUND_REQUEST,
+        TRACK_SYSTEM_DEFAULT,
+        LISTEN_FOR_BEST,
+    };
+
+    /**
+     * The type of the request. This is only used by the system and is always NONE elsewhere.
+     *
+     * @hide
+     */
+    public final Type type;
+
+    /**
+     * @hide
+     */
+    public NetworkRequest(NetworkCapabilities nc, int legacyType, int rId, Type type) {
+        if (nc == null) {
+            throw new NullPointerException();
+        }
+        requestId = rId;
+        networkCapabilities = nc;
+        this.legacyType = legacyType;
+        this.type = type;
+    }
+
+    /**
+     * @hide
+     */
+    public NetworkRequest(NetworkRequest that) {
+        networkCapabilities = new NetworkCapabilities(that.networkCapabilities);
+        requestId = that.requestId;
+        this.legacyType = that.legacyType;
+        this.type = that.type;
+    }
+
+    /**
+     * Builder used to create {@link NetworkRequest} objects.  Specify the Network features
+     * needed in terms of {@link NetworkCapabilities} features
+     */
+    public static class Builder {
+        /**
+         * Capabilities that are currently compatible with VCN networks.
+         */
+        private static final List<Integer> VCN_SUPPORTED_CAPABILITIES = Arrays.asList(
+                NET_CAPABILITY_CAPTIVE_PORTAL,
+                NET_CAPABILITY_DUN,
+                NET_CAPABILITY_FOREGROUND,
+                NET_CAPABILITY_INTERNET,
+                NET_CAPABILITY_NOT_CONGESTED,
+                NET_CAPABILITY_NOT_METERED,
+                NET_CAPABILITY_NOT_RESTRICTED,
+                NET_CAPABILITY_NOT_ROAMING,
+                NET_CAPABILITY_NOT_SUSPENDED,
+                NET_CAPABILITY_NOT_VPN,
+                NET_CAPABILITY_PARTIAL_CONNECTIVITY,
+                NET_CAPABILITY_TEMPORARILY_NOT_METERED,
+                NET_CAPABILITY_TRUSTED,
+                NET_CAPABILITY_VALIDATED);
+
+        private final NetworkCapabilities mNetworkCapabilities;
+
+        // A boolean that represents whether the NOT_VCN_MANAGED capability should be deduced when
+        // the NetworkRequest object is built.
+        private boolean mShouldDeduceNotVcnManaged = true;
+
+        /**
+         * Default constructor for Builder.
+         */
+        public Builder() {
+            // By default, restrict this request to networks available to this app.
+            // Apps can rescind this restriction, but ConnectivityService will enforce
+            // it for apps that do not have the NETWORK_SETTINGS permission.
+            mNetworkCapabilities = new NetworkCapabilities();
+            mNetworkCapabilities.setSingleUid(Process.myUid());
+        }
+
+        /**
+         * Creates a new Builder of NetworkRequest from an existing instance.
+         */
+        public Builder(@NonNull final NetworkRequest request) {
+            Objects.requireNonNull(request);
+            mNetworkCapabilities = request.networkCapabilities;
+            // If the caller constructed the builder from a request, it means the user
+            // might explicitly want the capabilities from the request. Thus, the NOT_VCN_MANAGED
+            // capabilities should not be touched later.
+            mShouldDeduceNotVcnManaged = false;
+        }
+
+        /**
+         * Build {@link NetworkRequest} give the current set of capabilities.
+         */
+        public NetworkRequest build() {
+            // Make a copy of mNetworkCapabilities so we don't inadvertently remove NOT_RESTRICTED
+            // when later an unrestricted capability could be added to mNetworkCapabilities, in
+            // which case NOT_RESTRICTED should be returned to mNetworkCapabilities, which
+            // maybeMarkCapabilitiesRestricted() doesn't add back.
+            final NetworkCapabilities nc = new NetworkCapabilities(mNetworkCapabilities);
+            nc.maybeMarkCapabilitiesRestricted();
+            deduceNotVcnManagedCapability(nc);
+            return new NetworkRequest(nc, ConnectivityManager.TYPE_NONE,
+                    ConnectivityManager.REQUEST_ID_UNSET, Type.NONE);
+        }
+
+        /**
+         * Add the given capability requirement to this builder.  These represent
+         * the requested network's required capabilities.  Note that when searching
+         * for a network to satisfy a request, all capabilities requested must be
+         * satisfied.
+         *
+         * @param capability The capability to add.
+         * @return The builder to facilitate chaining
+         *         {@code builder.addCapability(...).addCapability();}.
+         */
+        public Builder addCapability(@NetworkCapabilities.NetCapability int capability) {
+            mNetworkCapabilities.addCapability(capability);
+            if (capability == NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) {
+                mShouldDeduceNotVcnManaged = false;
+            }
+            return this;
+        }
+
+        /**
+         * Removes (if found) the given capability from this builder instance.
+         *
+         * @param capability The capability to remove.
+         * @return The builder to facilitate chaining.
+         */
+        public Builder removeCapability(@NetworkCapabilities.NetCapability int capability) {
+            mNetworkCapabilities.removeCapability(capability);
+            if (capability == NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) {
+                mShouldDeduceNotVcnManaged = false;
+            }
+            return this;
+        }
+
+        /**
+         * Set the {@code NetworkCapabilities} for this builder instance,
+         * overriding any capabilities that had been previously set.
+         *
+         * @param nc The superseding {@code NetworkCapabilities} instance.
+         * @return The builder to facilitate chaining.
+         * @hide
+         */
+        public Builder setCapabilities(NetworkCapabilities nc) {
+            mNetworkCapabilities.set(nc);
+            return this;
+        }
+
+        /**
+         * Set the watched UIDs for this request. This will be reset and wiped out unless
+         * the calling app holds the CHANGE_NETWORK_STATE permission.
+         *
+         * @param uids The watched UIDs as a set of {@code Range<Integer>}, or null for everything.
+         * @return The builder to facilitate chaining.
+         * @hide
+         */
+        @NonNull
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+        @SuppressLint("MissingGetterMatchingBuilder")
+        public Builder setUids(@Nullable Set<Range<Integer>> uids) {
+            mNetworkCapabilities.setUids(uids);
+            return this;
+        }
+
+        /**
+         * Add a capability that must not exist in the requested network.
+         * <p>
+         * If the capability was previously added to the list of required capabilities (for
+         * example, it was there by default or added using {@link #addCapability(int)} method), then
+         * it will be removed from the list of required capabilities as well.
+         *
+         * @see #addCapability(int)
+         *
+         * @param capability The capability to add to forbidden capability list.
+         * @return The builder to facilitate chaining.
+         *
+         * @hide
+         */
+        @NonNull
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+        public Builder addForbiddenCapability(@NetworkCapabilities.NetCapability int capability) {
+            mNetworkCapabilities.addForbiddenCapability(capability);
+            return this;
+        }
+
+        /**
+         * Removes (if found) the given forbidden capability from this builder instance.
+         *
+         * @param capability The forbidden capability to remove.
+         * @return The builder to facilitate chaining.
+         *
+         * @hide
+         */
+        @NonNull
+        @SuppressLint("BuilderSetStyle")
+        @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+        public Builder removeForbiddenCapability(
+                @NetworkCapabilities.NetCapability int capability) {
+            mNetworkCapabilities.removeForbiddenCapability(capability);
+            return this;
+        }
+
+        /**
+         * Completely clears all the {@code NetworkCapabilities} from this builder instance,
+         * removing even the capabilities that are set by default when the object is constructed.
+         *
+         * @return The builder to facilitate chaining.
+         */
+        @NonNull
+        public Builder clearCapabilities() {
+            mNetworkCapabilities.clearAll();
+            // If the caller explicitly clear all capabilities, the NOT_VCN_MANAGED capabilities
+            // should not be add back later.
+            mShouldDeduceNotVcnManaged = false;
+            return this;
+        }
+
+        /**
+         * Adds the given transport requirement to this builder.  These represent
+         * the set of allowed transports for the request.  Only networks using one
+         * of these transports will satisfy the request.  If no particular transports
+         * are required, none should be specified here.
+         *
+         * @param transportType The transport type to add.
+         * @return The builder to facilitate chaining.
+         */
+        public Builder addTransportType(@NetworkCapabilities.Transport int transportType) {
+            mNetworkCapabilities.addTransportType(transportType);
+            return this;
+        }
+
+        /**
+         * Removes (if found) the given transport from this builder instance.
+         *
+         * @param transportType The transport type to remove.
+         * @return The builder to facilitate chaining.
+         */
+        public Builder removeTransportType(@NetworkCapabilities.Transport int transportType) {
+            mNetworkCapabilities.removeTransportType(transportType);
+            return this;
+        }
+
+        /**
+         * @hide
+         */
+        public Builder setLinkUpstreamBandwidthKbps(int upKbps) {
+            mNetworkCapabilities.setLinkUpstreamBandwidthKbps(upKbps);
+            return this;
+        }
+        /**
+         * @hide
+         */
+        public Builder setLinkDownstreamBandwidthKbps(int downKbps) {
+            mNetworkCapabilities.setLinkDownstreamBandwidthKbps(downKbps);
+            return this;
+        }
+
+        /**
+         * Sets the optional bearer specific network specifier.
+         * This has no meaning if a single transport is also not specified, so calling
+         * this without a single transport set will generate an exception, as will
+         * subsequently adding or removing transports after this is set.
+         * </p>
+         * If the {@code networkSpecifier} is provided, it shall be interpreted as follows:
+         * <ul>
+         * <li>If the specifier can be parsed as an integer, it will be treated as a
+         * {@link android.net TelephonyNetworkSpecifier}, and the provided integer will be
+         * interpreted as a SubscriptionId.
+         * <li>If the value is an ethernet interface name, it will be treated as such.
+         * <li>For all other cases, the behavior is undefined.
+         * </ul>
+         *
+         * @param networkSpecifier A {@code String} of either a SubscriptionId in cellular
+         *                         network request or an ethernet interface name in ethernet
+         *                         network request.
+         *
+         * @deprecated Use {@link #setNetworkSpecifier(NetworkSpecifier)} instead.
+         */
+        @Deprecated
+        public Builder setNetworkSpecifier(String networkSpecifier) {
+            try {
+                int subId = Integer.parseInt(networkSpecifier);
+                return setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
+                        .setSubscriptionId(subId).build());
+            } catch (NumberFormatException nfe) {
+                // An EthernetNetworkSpecifier or TestNetworkSpecifier does not accept null or empty
+                // ("") strings. When network specifiers were strings a null string and an empty
+                // string were considered equivalent. Hence no meaning is attached to a null or
+                // empty ("") string.
+                if (TextUtils.isEmpty(networkSpecifier)) {
+                    return setNetworkSpecifier((NetworkSpecifier) null);
+                } else if (mNetworkCapabilities.hasTransport(TRANSPORT_TEST)) {
+                    return setNetworkSpecifier(new TestNetworkSpecifier(networkSpecifier));
+                } else {
+                    return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));
+                }
+            }
+        }
+
+        /**
+         * Sets the optional bearer specific network specifier.
+         * This has no meaning if a single transport is also not specified, so calling
+         * this without a single transport set will generate an exception, as will
+         * subsequently adding or removing transports after this is set.
+         * </p>
+         *
+         * @param networkSpecifier A concrete, parcelable framework class that extends
+         *                         NetworkSpecifier.
+         */
+        public Builder setNetworkSpecifier(NetworkSpecifier networkSpecifier) {
+            if (networkSpecifier instanceof MatchAllNetworkSpecifier) {
+                throw new IllegalArgumentException("A MatchAllNetworkSpecifier is not permitted");
+            }
+            mNetworkCapabilities.setNetworkSpecifier(networkSpecifier);
+            // Do not touch NOT_VCN_MANAGED if the caller needs to access to a very specific
+            // Network.
+            mShouldDeduceNotVcnManaged = false;
+            return this;
+        }
+
+        /**
+         * Sets the signal strength. This is a signed integer, with higher values indicating a
+         * stronger signal. The exact units are bearer-dependent. For example, Wi-Fi uses the same
+         * RSSI units reported by WifiManager.
+         * <p>
+         * Note that when used to register a network callback, this specifies the minimum acceptable
+         * signal strength. When received as the state of an existing network it specifies the
+         * current value. A value of {@code SIGNAL_STRENGTH_UNSPECIFIED} means no value when
+         * received and has no effect when requesting a callback.
+         *
+         * <p>This method requires the caller to hold the
+         * {@link android.Manifest.permission#NETWORK_SIGNAL_STRENGTH_WAKEUP} permission
+         *
+         * @param signalStrength the bearer-specific signal strength.
+         * @hide
+         */
+        @SystemApi
+        @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP)
+        public @NonNull Builder setSignalStrength(int signalStrength) {
+            mNetworkCapabilities.setSignalStrength(signalStrength);
+            return this;
+        }
+
+        /**
+         * Deduce the NET_CAPABILITY_NOT_VCN_MANAGED capability from other capabilities
+         * and user intention, which includes:
+         *   1. For the requests that don't have anything besides
+         *      {@link #VCN_SUPPORTED_CAPABILITIES}, add the NET_CAPABILITY_NOT_VCN_MANAGED to
+         *      allow the callers automatically utilize VCN networks if available.
+         *   2. For the requests that explicitly add or remove NET_CAPABILITY_NOT_VCN_MANAGED,
+         *      or has clear intention of tracking specific network,
+         *      do not alter them to allow user fire request that suits their need.
+         *
+         * @hide
+         */
+        private void deduceNotVcnManagedCapability(final NetworkCapabilities nc) {
+            if (!mShouldDeduceNotVcnManaged) return;
+            for (final int cap : nc.getCapabilities()) {
+                if (!VCN_SUPPORTED_CAPABILITIES.contains(cap)) return;
+            }
+            nc.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        }
+
+        /**
+         * Sets the optional subscription ID set.
+         * <p>
+         * This specify the subscription IDs requirement.
+         * A network will satisfy this request only if it matches one of the subIds in this set.
+         * An empty set matches all networks, including those without a subId.
+         *
+         * <p>Registering a NetworkRequest with a non-empty set of subIds requires the
+         * NETWORK_FACTORY permission.
+         *
+         * @param subIds A {@code Set} that represents subscription IDs.
+         * @hide
+         */
+        @NonNull
+        @SystemApi
+        public Builder setSubscriptionIds(@NonNull Set<Integer> subIds) {
+            mNetworkCapabilities.setSubscriptionIds(subIds);
+            return this;
+        }
+    }
+
+    // implement the Parcelable interface
+    public int describeContents() {
+        return 0;
+    }
+    public void writeToParcel(Parcel dest, int flags) {
+        networkCapabilities.writeToParcel(dest, flags);
+        dest.writeInt(legacyType);
+        dest.writeInt(requestId);
+        dest.writeString(type.name());
+    }
+
+    public static final @android.annotation.NonNull Creator<NetworkRequest> CREATOR =
+        new Creator<NetworkRequest>() {
+            public NetworkRequest createFromParcel(Parcel in) {
+                NetworkCapabilities nc = NetworkCapabilities.CREATOR.createFromParcel(in);
+                int legacyType = in.readInt();
+                int requestId = in.readInt();
+                Type type = Type.valueOf(in.readString());  // IllegalArgumentException if invalid.
+                NetworkRequest result = new NetworkRequest(nc, legacyType, requestId, type);
+                return result;
+            }
+            public NetworkRequest[] newArray(int size) {
+                return new NetworkRequest[size];
+            }
+        };
+
+    /**
+     * Returns true iff. this NetworkRequest is of type LISTEN.
+     *
+     * @hide
+     */
+    public boolean isListen() {
+        return type == Type.LISTEN;
+    }
+
+    /**
+     * Returns true iff. this NetworkRequest is of type LISTEN_FOR_BEST.
+     *
+     * @hide
+     */
+    public boolean isListenForBest() {
+        return type == Type.LISTEN_FOR_BEST;
+    }
+
+    /**
+     * Returns true iff. the contained NetworkRequest is one that:
+     *
+     *     - should be associated with at most one satisfying network
+     *       at a time;
+     *
+     *     - should cause a network to be kept up, but not necessarily in
+     *       the foreground, if it is the best network which can satisfy the
+     *       NetworkRequest.
+     *
+     * For full detail of how isRequest() is used for pairing Networks with
+     * NetworkRequests read rematchNetworkAndRequests().
+     *
+     * @hide
+     */
+    public boolean isRequest() {
+        return type == Type.REQUEST || type == Type.BACKGROUND_REQUEST;
+    }
+
+    /**
+     * Returns true iff. this NetworkRequest is of type BACKGROUND_REQUEST.
+     *
+     * @hide
+     */
+    public boolean isBackgroundRequest() {
+        return type == Type.BACKGROUND_REQUEST;
+    }
+
+    /**
+     * @see Builder#addCapability(int)
+     */
+    public boolean hasCapability(@NetCapability int capability) {
+        return networkCapabilities.hasCapability(capability);
+    }
+
+    /**
+     * @see Builder#addForbiddenCapability(int)
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public boolean hasForbiddenCapability(@NetCapability int capability) {
+        return networkCapabilities.hasForbiddenCapability(capability);
+    }
+
+    /**
+     * Returns true if and only if the capabilities requested in this NetworkRequest are satisfied
+     * by the provided {@link NetworkCapabilities}.
+     *
+     * @param nc Capabilities that should satisfy this NetworkRequest. null capabilities do not
+     *           satisfy any request.
+     */
+    public boolean canBeSatisfiedBy(@Nullable NetworkCapabilities nc) {
+        return networkCapabilities.satisfiedByNetworkCapabilities(nc);
+    }
+
+    /**
+     * @see Builder#addTransportType(int)
+     */
+    public boolean hasTransport(@Transport int transportType) {
+        return networkCapabilities.hasTransport(transportType);
+    }
+
+    /**
+     * @see Builder#setNetworkSpecifier(NetworkSpecifier)
+     */
+    @Nullable
+    public NetworkSpecifier getNetworkSpecifier() {
+        return networkCapabilities.getNetworkSpecifier();
+    }
+
+    /**
+     * @return the uid of the app making the request.
+     *
+     * Note: This could return {@link Process#INVALID_UID} if the {@link NetworkRequest} object was
+     * not obtained from {@link ConnectivityManager}.
+     * @hide
+     */
+    @SystemApi
+    public int getRequestorUid() {
+        return networkCapabilities.getRequestorUid();
+    }
+
+    /**
+     * @return the package name of the app making the request.
+     *
+     * Note: This could return {@code null} if the {@link NetworkRequest} object was not obtained
+     * from {@link ConnectivityManager}.
+     * @hide
+     */
+    @SystemApi
+    @Nullable
+    public String getRequestorPackageName() {
+        return networkCapabilities.getRequestorPackageName();
+    }
+
+    public String toString() {
+        return "NetworkRequest [ " + type + " id=" + requestId +
+                (legacyType != ConnectivityManager.TYPE_NONE ? ", legacyType=" + legacyType : "") +
+                ", " + networkCapabilities.toString() + " ]";
+    }
+
+    private int typeToProtoEnum(Type t) {
+        switch (t) {
+            case NONE:
+                return NetworkRequestProto.TYPE_NONE;
+            case LISTEN:
+                return NetworkRequestProto.TYPE_LISTEN;
+            case TRACK_DEFAULT:
+                return NetworkRequestProto.TYPE_TRACK_DEFAULT;
+            case REQUEST:
+                return NetworkRequestProto.TYPE_REQUEST;
+            case BACKGROUND_REQUEST:
+                return NetworkRequestProto.TYPE_BACKGROUND_REQUEST;
+            case TRACK_SYSTEM_DEFAULT:
+                return NetworkRequestProto.TYPE_TRACK_SYSTEM_DEFAULT;
+            default:
+                return NetworkRequestProto.TYPE_UNKNOWN;
+        }
+    }
+
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof NetworkRequest == false) return false;
+        NetworkRequest that = (NetworkRequest)obj;
+        return (that.legacyType == this.legacyType &&
+                that.requestId == this.requestId &&
+                that.type == this.type &&
+                Objects.equals(that.networkCapabilities, this.networkCapabilities));
+    }
+
+    public int hashCode() {
+        return Objects.hash(requestId, legacyType, networkCapabilities, type);
+    }
+
+    /**
+     * Gets all the capabilities set on this {@code NetworkRequest} instance.
+     *
+     * @return an array of capability values for this instance.
+     */
+    @NonNull
+    public @NetCapability int[] getCapabilities() {
+        // No need to make a defensive copy here as NC#getCapabilities() already returns
+        // a new array.
+        return networkCapabilities.getCapabilities();
+    }
+
+    /**
+     * Gets all the forbidden capabilities set on this {@code NetworkRequest} instance.
+     *
+     * @return an array of forbidden capability values for this instance.
+     *
+     * @hide
+     */
+    @NonNull
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public @NetCapability int[] getForbiddenCapabilities() {
+        // No need to make a defensive copy here as NC#getForbiddenCapabilities() already returns
+        // a new array.
+        return networkCapabilities.getForbiddenCapabilities();
+    }
+
+    /**
+     * Gets all the transports set on this {@code NetworkRequest} instance.
+     *
+     * @return an array of transport type values for this instance.
+     */
+    @NonNull
+    public @Transport int[] getTransportTypes() {
+        // No need to make a defensive copy here as NC#getTransportTypes() already returns
+        // a new array.
+        return networkCapabilities.getTransportTypes();
+    }
+}
diff --git a/framework/src/android/net/NetworkScore.java b/framework/src/android/net/NetworkScore.java
new file mode 100644
index 0000000..7be7deb
--- /dev/null
+++ b/framework/src/android/net/NetworkScore.java
@@ -0,0 +1,328 @@
+/*
+ * 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.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Object representing the quality of a network as perceived by the user.
+ *
+ * A NetworkScore object represents the characteristics of a network that affects how good the
+ * network is considered for a particular use.
+ * @hide
+ */
+@SystemApi
+public final class NetworkScore implements Parcelable {
+    // 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;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            KEEP_CONNECTED_NONE,
+            KEEP_CONNECTED_FOR_HANDOVER
+    })
+    public @interface KeepConnectedReason { }
+
+    /**
+     * Do not keep this network connected if there is no outstanding request for it.
+     */
+    public static final int KEEP_CONNECTED_NONE = 0;
+    /**
+     * Keep this network connected even if there is no outstanding request for it, because it
+     * is being considered for handover.
+     */
+    public static final int KEEP_CONNECTED_FOR_HANDOVER = 1;
+
+    // Agent-managed policies
+    // This network should lose to a wifi that has ever been validated
+    // NOTE : temporarily this policy is managed by ConnectivityService, because of legacy. The
+    // legacy design has this bit global to the system and tacked on WiFi which means it will affect
+    // networks from carriers who don't want it and non-carrier networks, which is bad for users.
+    // The S design has this on mobile networks only, so this can be fixed eventually ; as CS
+    // doesn't know what carriers need this bit, the initial S implementation will continue to
+    // affect other carriers but will at least leave non-mobile networks alone. Eventually Telephony
+    // should set this on networks from carriers that require it.
+    /** @hide */
+    public static final int POLICY_YIELD_TO_BAD_WIFI = 1;
+    // This network is primary for this transport.
+    /** @hide */
+    public static final int POLICY_TRANSPORT_PRIMARY = 2;
+    // This network is exiting : it will likely disconnect in a few seconds.
+    /** @hide */
+    public static final int POLICY_EXITING = 3;
+
+    /** @hide */
+    public static final int MIN_AGENT_MANAGED_POLICY = POLICY_YIELD_TO_BAD_WIFI;
+    /** @hide */
+    public static final int MAX_AGENT_MANAGED_POLICY = POLICY_EXITING;
+
+    // Bitmask of all the policies applied to this score.
+    private final long mPolicies;
+
+    private final int mKeepConnectedReason;
+
+    /** @hide */
+    NetworkScore(final int legacyInt, final long policies,
+            @KeepConnectedReason final int keepConnectedReason) {
+        mLegacyInt = legacyInt;
+        mPolicies = policies;
+        mKeepConnectedReason = keepConnectedReason;
+    }
+
+    private NetworkScore(@NonNull final Parcel in) {
+        mLegacyInt = in.readInt();
+        mPolicies = in.readLong();
+        mKeepConnectedReason = in.readInt();
+    }
+
+    /**
+     * Get the legacy int score embedded in this NetworkScore.
+     * @see Builder#setLegacyInt(int)
+     */
+    public int getLegacyInt() {
+        return mLegacyInt;
+    }
+
+    /**
+     * Returns the keep-connected reason, or KEEP_CONNECTED_NONE.
+     */
+    public int getKeepConnectedReason() {
+        return mKeepConnectedReason;
+    }
+
+    /**
+     * @return whether this score has a particular policy.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public boolean hasPolicy(final int policy) {
+        return 0 != (mPolicies & (1L << policy));
+    }
+
+    /**
+     * To the exclusive usage of FullScore
+     * @hide
+     */
+    public long getPolicies() {
+        return mPolicies;
+    }
+
+    /**
+     * Whether this network should yield to a previously validated wifi gone bad.
+     *
+     * If this policy is set, other things being equal, the device will prefer a previously
+     * validated WiFi even if this network is validated and the WiFi is not.
+     * If this policy is not set, the device prefers the validated network.
+     *
+     * @hide
+     */
+    // TODO : Unhide this for telephony and have telephony call it on the relevant carriers.
+    // In the mean time this is handled by Connectivity in a backward-compatible manner.
+    public boolean shouldYieldToBadWifi() {
+        return hasPolicy(POLICY_YIELD_TO_BAD_WIFI);
+    }
+
+    /**
+     * Whether this network is primary for this transport.
+     *
+     * When multiple networks of the same transport are active, the device prefers the ones that
+     * are primary. This is meant in particular for DS-DA devices with a user setting to choose the
+     * default SIM card, or for WiFi STA+STA and make-before-break cases.
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean isTransportPrimary() {
+        return hasPolicy(POLICY_TRANSPORT_PRIMARY);
+    }
+
+    /**
+     * Whether this network is exiting.
+     *
+     * If this policy is set, the device will expect this network to disconnect within seconds.
+     * It will try to migrate to some other network if any is available, policy permitting, to
+     * avoid service disruption.
+     * This is useful in particular when a good cellular network is available and WiFi is getting
+     * weak and risks disconnecting soon. The WiFi network should be marked as exiting so that
+     * the device will prefer the reliable mobile network over this soon-to-be-lost WiFi.
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean isExiting() {
+        return hasPolicy(POLICY_EXITING);
+    }
+
+    @Override
+    public String toString() {
+        return "Score(" + mLegacyInt + " ; Policies : " + mPolicies + ")";
+    }
+
+    @Override
+    public void writeToParcel(@NonNull final Parcel dest, final int flags) {
+        dest.writeInt(mLegacyInt);
+        dest.writeLong(mPolicies);
+        dest.writeInt(mKeepConnectedReason);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull public static final Creator<NetworkScore> CREATOR = new Creator<>() {
+        @Override
+        @NonNull
+        public NetworkScore createFromParcel(@NonNull final Parcel in) {
+            return new NetworkScore(in);
+        }
+
+        @Override
+        @NonNull
+        public NetworkScore[] newArray(int size) {
+            return new NetworkScore[size];
+        }
+    };
+
+    /**
+     * A builder for NetworkScore.
+     */
+    public static final class Builder {
+        private static final long POLICY_NONE = 0L;
+        private static final int INVALID_LEGACY_INT = Integer.MIN_VALUE;
+        private int mLegacyInt = INVALID_LEGACY_INT;
+        private int mKeepConnectedReason = KEEP_CONNECTED_NONE;
+        private int mPolicies = 0;
+
+        /**
+         * Sets the legacy int for this score.
+         *
+         * This will be used for measurements and logs, but will no longer be used for ranking
+         * networks against each other. Callers that existed before Android S should send what
+         * they used to send as the int score.
+         *
+         * @param score the legacy int
+         * @return this
+         */
+        @NonNull
+        public Builder setLegacyInt(final int score) {
+            mLegacyInt = score;
+            return this;
+        }
+
+
+        /**
+         * Set for a network that should never be preferred to a wifi that has ever been validated
+         *
+         * If this policy is set, other things being equal, the device will prefer a previously
+         * validated WiFi even if this network is validated and the WiFi is not.
+         * If this policy is not set, the device prefers the validated network.
+         *
+         * @return this builder
+         * @hide
+         */
+        // TODO : Unhide this for telephony and have telephony call it on the relevant carriers.
+        // In the mean time this is handled by Connectivity in a backward-compatible manner.
+        @NonNull
+        public Builder setShouldYieldToBadWifi(final boolean val) {
+            if (val) {
+                mPolicies |= (1L << POLICY_YIELD_TO_BAD_WIFI);
+            } else {
+                mPolicies &= ~(1L << POLICY_YIELD_TO_BAD_WIFI);
+            }
+            return this;
+        }
+
+        /**
+         * Set for a network that is primary for this transport.
+         *
+         * When multiple networks of the same transport are active, the device prefers the ones that
+         * are primary. This is meant in particular for DS-DA devices with a user setting to choose
+         * the default SIM card, or for WiFi STA+STA and make-before-break cases.
+         *
+         * @return this builder
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public Builder setTransportPrimary(final boolean val) {
+            if (val) {
+                mPolicies |= (1L << POLICY_TRANSPORT_PRIMARY);
+            } else {
+                mPolicies &= ~(1L << POLICY_TRANSPORT_PRIMARY);
+            }
+            return this;
+        }
+
+        /**
+         * Set for a network that will likely disconnect in a few seconds.
+         *
+         * If this policy is set, the device will expect this network to disconnect within seconds.
+         * It will try to migrate to some other network if any is available, policy permitting, to
+         * avoid service disruption.
+         * This is useful in particular when a good cellular network is available and WiFi is
+         * getting weak and risks disconnecting soon. The WiFi network should be marked as exiting
+         * so that the device will prefer the reliable mobile network over this soon-to-be-lost
+         * WiFi.
+         *
+         * @return this builder
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        public Builder setExiting(final boolean val) {
+            if (val) {
+                mPolicies |= (1L << POLICY_EXITING);
+            } else {
+                mPolicies &= ~(1L << POLICY_EXITING);
+            }
+            return this;
+        }
+
+        /**
+         * Set the keep-connected reason.
+         *
+         * This can be reset by calling it again with {@link KEEP_CONNECTED_NONE}.
+         */
+        @NonNull
+        public Builder setKeepConnectedReason(@KeepConnectedReason final int reason) {
+            mKeepConnectedReason = reason;
+            return this;
+        }
+
+        /**
+         * Builds this NetworkScore.
+         * @return The built NetworkScore object.
+         */
+        @NonNull
+        public NetworkScore build() {
+            return new NetworkScore(mLegacyInt, mPolicies, mKeepConnectedReason);
+        }
+    }
+}
diff --git a/framework/src/android/net/NetworkState.java b/framework/src/android/net/NetworkState.java
new file mode 100644
index 0000000..9b69674
--- /dev/null
+++ b/framework/src/android/net/NetworkState.java
@@ -0,0 +1,130 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+/**
+ * Snapshot of network state.
+ *
+ * @hide
+ */
+public class NetworkState implements Parcelable {
+    private static final boolean VALIDATE_ROAMING_STATE = false;
+
+    // TODO: remove and make members @NonNull.
+    public static final NetworkState EMPTY = new NetworkState();
+
+    public final NetworkInfo networkInfo;
+    public final LinkProperties linkProperties;
+    public final NetworkCapabilities networkCapabilities;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public final Network network;
+    public final String subscriberId;
+    public final int legacyNetworkType;
+
+    private NetworkState() {
+        networkInfo = null;
+        linkProperties = null;
+        networkCapabilities = null;
+        network = null;
+        subscriberId = null;
+        legacyNetworkType = 0;
+    }
+
+    public NetworkState(int legacyNetworkType, @NonNull LinkProperties linkProperties,
+            @NonNull NetworkCapabilities networkCapabilities, @NonNull Network network,
+            @Nullable String subscriberId) {
+        this(legacyNetworkType, new NetworkInfo(legacyNetworkType, 0, null, null), linkProperties,
+                networkCapabilities, network, subscriberId);
+    }
+
+    // Constructor that used internally in ConnectivityService mainline module.
+    public NetworkState(@NonNull NetworkInfo networkInfo, @NonNull LinkProperties linkProperties,
+            @NonNull NetworkCapabilities networkCapabilities, @NonNull Network network,
+            @Nullable String subscriberId) {
+        this(networkInfo.getType(), networkInfo, linkProperties,
+                networkCapabilities, network, subscriberId);
+    }
+
+    public NetworkState(int legacyNetworkType, @NonNull NetworkInfo networkInfo,
+            @NonNull LinkProperties linkProperties,
+            @NonNull NetworkCapabilities networkCapabilities, @NonNull Network network,
+            @Nullable String subscriberId) {
+        this.networkInfo = networkInfo;
+        this.linkProperties = linkProperties;
+        this.networkCapabilities = networkCapabilities;
+        this.network = network;
+        this.subscriberId = subscriberId;
+        this.legacyNetworkType = legacyNetworkType;
+
+        // This object is an atomic view of a network, so the various components
+        // should always agree on roaming state.
+        if (VALIDATE_ROAMING_STATE && networkInfo != null && networkCapabilities != null) {
+            if (networkInfo.isRoaming() == networkCapabilities
+                    .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)) {
+                Log.wtf("NetworkState", "Roaming state disagreement between " + networkInfo
+                        + " and " + networkCapabilities);
+            }
+        }
+    }
+
+    @UnsupportedAppUsage
+    public NetworkState(Parcel in) {
+        networkInfo = in.readParcelable(null);
+        linkProperties = in.readParcelable(null);
+        networkCapabilities = in.readParcelable(null);
+        network = in.readParcelable(null);
+        subscriberId = in.readString();
+        legacyNetworkType = in.readInt();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeParcelable(networkInfo, flags);
+        out.writeParcelable(linkProperties, flags);
+        out.writeParcelable(networkCapabilities, flags);
+        out.writeParcelable(network, flags);
+        out.writeString(subscriberId);
+        out.writeInt(legacyNetworkType);
+    }
+
+    @UnsupportedAppUsage
+    @NonNull
+    public static final Creator<NetworkState> CREATOR = new Creator<NetworkState>() {
+        @Override
+        public NetworkState createFromParcel(Parcel in) {
+            return new NetworkState(in);
+        }
+
+        @Override
+        public NetworkState[] newArray(int size) {
+            return new NetworkState[size];
+        }
+    };
+}
diff --git a/framework/src/android/net/NetworkUtils.java b/framework/src/android/net/NetworkUtils.java
new file mode 100644
index 0000000..16a49bc
--- /dev/null
+++ b/framework/src/android/net/NetworkUtils.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.NETID_UNSET;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.net.module.util.Inet4AddressUtils;
+
+import java.io.FileDescriptor;
+import java.math.BigInteger;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Locale;
+import java.util.TreeSet;
+
+/**
+ * Native methods for managing network interfaces.
+ *
+ * {@hide}
+ */
+public class NetworkUtils {
+
+    private static final String TAG = "NetworkUtils";
+
+    /**
+     * Attaches a socket filter that drops all of incoming packets.
+     * @param fd the socket's {@link FileDescriptor}.
+     */
+    public static native void attachDropAllBPFFilter(FileDescriptor fd) throws SocketException;
+
+    /**
+     * Detaches a socket filter.
+     * @param fd the socket's {@link FileDescriptor}.
+     */
+    public static native void detachBPFFilter(FileDescriptor fd) throws SocketException;
+
+    private static native boolean bindProcessToNetworkHandle(long netHandle);
+
+    /**
+     * Binds the current process to the network designated by {@code netId}.  All sockets created
+     * in the future (and not explicitly bound via a bound {@link SocketFactory} (see
+     * {@link Network#getSocketFactory}) will be bound to this network.  Note that if this
+     * {@code Network} ever disconnects all sockets created in this way will cease to work.  This
+     * is by design so an application doesn't accidentally use sockets it thinks are still bound to
+     * a particular {@code Network}.  Passing NETID_UNSET clears the binding.
+     */
+    public static boolean bindProcessToNetwork(int netId) {
+        return bindProcessToNetworkHandle(new Network(netId).getNetworkHandle());
+    }
+
+    private static native long getBoundNetworkHandleForProcess();
+
+    /**
+     * Return the netId last passed to {@link #bindProcessToNetwork}, or NETID_UNSET if
+     * {@link #unbindProcessToNetwork} has been called since {@link #bindProcessToNetwork}.
+     */
+    public static int getBoundNetworkForProcess() {
+        final long netHandle = getBoundNetworkHandleForProcess();
+        return netHandle == 0L ? NETID_UNSET : Network.fromNetworkHandle(netHandle).getNetId();
+    }
+
+    /**
+     * Binds host resolutions performed by this process to the network designated by {@code netId}.
+     * {@link #bindProcessToNetwork} takes precedence over this setting.  Passing NETID_UNSET clears
+     * the binding.
+     *
+     * @deprecated This is strictly for legacy usage to support startUsingNetworkFeature().
+     */
+    @Deprecated
+    public native static boolean bindProcessToNetworkForHostResolution(int netId);
+
+    private static native int bindSocketToNetworkHandle(FileDescriptor fd, long netHandle);
+
+    /**
+     * Explicitly binds {@code fd} to the network designated by {@code netId}.  This
+     * overrides any binding via {@link #bindProcessToNetwork}.
+     * @return 0 on success or negative errno on failure.
+     */
+    public static int bindSocketToNetwork(FileDescriptor fd, int netId) {
+        return bindSocketToNetworkHandle(fd, new Network(netId).getNetworkHandle());
+    }
+
+    /**
+     * Determine if {@code uid} can access network designated by {@code netId}.
+     * @return {@code true} if {@code uid} can access network, {@code false} otherwise.
+     */
+    public static boolean queryUserAccess(int uid, int netId) {
+        // TODO (b/183485986): remove this method
+        return false;
+    }
+
+    private static native FileDescriptor resNetworkSend(
+            long netHandle, byte[] msg, int msglen, int flags) throws ErrnoException;
+
+    /**
+     * DNS resolver series jni method.
+     * Issue the query {@code msg} on the network designated by {@code netId}.
+     * {@code flags} is an additional config to control actual querying behavior.
+     * @return a file descriptor to watch for read events
+     */
+    public static FileDescriptor resNetworkSend(
+            int netId, byte[] msg, int msglen, int flags) throws ErrnoException {
+        return resNetworkSend(new Network(netId).getNetworkHandle(), msg, msglen, flags);
+    }
+
+    private static native FileDescriptor resNetworkQuery(
+            long netHandle, String dname, int nsClass, int nsType, int flags) throws ErrnoException;
+
+    /**
+     * DNS resolver series jni method.
+     * Look up the {@code nsClass} {@code nsType} Resource Record (RR) associated
+     * with Domain Name {@code dname} on the network designated by {@code netId}.
+     * {@code flags} is an additional config to control actual querying behavior.
+     * @return a file descriptor to watch for read events
+     */
+    public static FileDescriptor resNetworkQuery(
+            int netId, String dname, int nsClass, int nsType, int flags) throws ErrnoException {
+        return resNetworkQuery(new Network(netId).getNetworkHandle(), dname, nsClass, nsType,
+                flags);
+    }
+
+    /**
+     * DNS resolver series jni method.
+     * Read a result for the query associated with the {@code fd}.
+     * @return DnsResponse containing blob answer and rcode
+     */
+    public static native DnsResolver.DnsResponse resNetworkResult(FileDescriptor fd)
+            throws ErrnoException;
+
+    /**
+     * DNS resolver series jni method.
+     * Attempts to cancel the in-progress query associated with the {@code fd}.
+     */
+    public static native void resNetworkCancel(FileDescriptor fd);
+
+    /**
+     * DNS resolver series jni method.
+     * Attempts to get network which resolver will use if no network is explicitly selected.
+     */
+    public static native Network getDnsNetwork() throws ErrnoException;
+
+    /**
+     * Get the tcp repair window associated with the {@code fd}.
+     *
+     * @param fd the tcp socket's {@link FileDescriptor}.
+     * @return a {@link TcpRepairWindow} object indicates tcp window size.
+     */
+    public static native TcpRepairWindow getTcpRepairWindow(FileDescriptor fd)
+            throws ErrnoException;
+
+    /**
+     * @see Inet4AddressUtils#intToInet4AddressHTL(int)
+     * @deprecated Use either {@link Inet4AddressUtils#intToInet4AddressHTH(int)}
+     *             or {@link Inet4AddressUtils#intToInet4AddressHTL(int)}
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static InetAddress intToInetAddress(int hostAddress) {
+        return Inet4AddressUtils.intToInet4AddressHTL(hostAddress);
+    }
+
+    /**
+     * @see Inet4AddressUtils#inet4AddressToIntHTL(Inet4Address)
+     * @deprecated Use either {@link Inet4AddressUtils#inet4AddressToIntHTH(Inet4Address)}
+     *             or {@link Inet4AddressUtils#inet4AddressToIntHTL(Inet4Address)}
+     */
+    @Deprecated
+    public static int inetAddressToInt(Inet4Address inetAddr)
+            throws IllegalArgumentException {
+        return Inet4AddressUtils.inet4AddressToIntHTL(inetAddr);
+    }
+
+    /**
+     * @see Inet4AddressUtils#prefixLengthToV4NetmaskIntHTL(int)
+     * @deprecated Use either {@link Inet4AddressUtils#prefixLengthToV4NetmaskIntHTH(int)}
+     *             or {@link Inet4AddressUtils#prefixLengthToV4NetmaskIntHTL(int)}
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static int prefixLengthToNetmaskInt(int prefixLength)
+            throws IllegalArgumentException {
+        return Inet4AddressUtils.prefixLengthToV4NetmaskIntHTL(prefixLength);
+    }
+
+    /**
+     * Convert a IPv4 netmask integer to a prefix length
+     * @param netmask as an integer (0xff000000 for a /8 subnet)
+     * @return the network prefix length
+     */
+    public static int netmaskIntToPrefixLength(int netmask) {
+        return Integer.bitCount(netmask);
+    }
+
+    /**
+     * Convert an IPv4 netmask to a prefix length, checking that the netmask is contiguous.
+     * @param netmask as a {@code Inet4Address}.
+     * @return the network prefix length
+     * @throws IllegalArgumentException the specified netmask was not contiguous.
+     * @hide
+     * @deprecated use {@link Inet4AddressUtils#netmaskToPrefixLength(Inet4Address)}
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    @Deprecated
+    public static int netmaskToPrefixLength(Inet4Address netmask) {
+        // This is only here because some apps seem to be using it (@UnsupportedAppUsage).
+        return Inet4AddressUtils.netmaskToPrefixLength(netmask);
+    }
+
+
+    /**
+     * Create an InetAddress from a string where the string must be a standard
+     * representation of a V4 or V6 address.  Avoids doing a DNS lookup on failure
+     * but it will throw an IllegalArgumentException in that case.
+     * @param addrString
+     * @return the InetAddress
+     * @hide
+     * @deprecated Use {@link InetAddresses#parseNumericAddress(String)}, if possible.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    @Deprecated
+    public static InetAddress numericToInetAddress(String addrString)
+            throws IllegalArgumentException {
+        return InetAddresses.parseNumericAddress(addrString);
+    }
+
+    /**
+     * Returns the implicit netmask of an IPv4 address, as was the custom before 1993.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public static int getImplicitNetmask(Inet4Address address) {
+        // Only here because it seems to be used by apps
+        return Inet4AddressUtils.getImplicitNetmask(address);
+    }
+
+    /**
+     * Utility method to parse strings such as "192.0.2.5/24" or "2001:db8::cafe:d00d/64".
+     * @hide
+     */
+    public static Pair<InetAddress, Integer> parseIpAndMask(String ipAndMaskString) {
+        InetAddress address = null;
+        int prefixLength = -1;
+        try {
+            String[] pieces = ipAndMaskString.split("/", 2);
+            prefixLength = Integer.parseInt(pieces[1]);
+            address = InetAddresses.parseNumericAddress(pieces[0]);
+        } catch (NullPointerException e) {            // Null string.
+        } catch (ArrayIndexOutOfBoundsException e) {  // No prefix length.
+        } catch (NumberFormatException e) {           // Non-numeric prefix.
+        } catch (IllegalArgumentException e) {        // Invalid IP address.
+        }
+
+        if (address == null || prefixLength == -1) {
+            throw new IllegalArgumentException("Invalid IP address and mask " + ipAndMaskString);
+        }
+
+        return new Pair<InetAddress, Integer>(address, prefixLength);
+    }
+
+    /**
+     * Utility method to parse strings such as "192.0.2.5/24" or "2001:db8::cafe:d00d/64".
+     * @hide
+     *
+     * @deprecated This method is used only for IpPrefix and LinkAddress. Since Android S, use
+     *             {@link #parseIpAndMask(String)}, if possible.
+     */
+    @Deprecated
+    public static Pair<InetAddress, Integer> legacyParseIpAndMask(String ipAndMaskString) {
+        InetAddress address = null;
+        int prefixLength = -1;
+        try {
+            String[] pieces = ipAndMaskString.split("/", 2);
+            prefixLength = Integer.parseInt(pieces[1]);
+            if (pieces[0] == null || pieces[0].isEmpty()) {
+                final byte[] bytes = new byte[16];
+                bytes[15] = 1;
+                return new Pair<InetAddress, Integer>(Inet6Address.getByAddress(
+                        "ip6-localhost"/* host */, bytes, 0 /* scope_id */), prefixLength);
+            }
+
+            if (pieces[0].startsWith("[")
+                    && pieces[0].endsWith("]")
+                    && pieces[0].indexOf(':') != -1) {
+                pieces[0] = pieces[0].substring(1, pieces[0].length() - 1);
+            }
+            address = InetAddresses.parseNumericAddress(pieces[0]);
+        } catch (NullPointerException e) {            // Null string.
+        } catch (ArrayIndexOutOfBoundsException e) {  // No prefix length.
+        } catch (NumberFormatException e) {           // Non-numeric prefix.
+        } catch (IllegalArgumentException e) {        // Invalid IP address.
+        } catch (UnknownHostException e) {            // IP address length is illegal
+        }
+
+        if (address == null || prefixLength == -1) {
+            throw new IllegalArgumentException("Invalid IP address and mask " + ipAndMaskString);
+        }
+
+        return new Pair<InetAddress, Integer>(address, prefixLength);
+    }
+
+    /**
+     * Convert a 32 char hex string into a Inet6Address.
+     * throws a runtime exception if the string isn't 32 chars, isn't hex or can't be
+     * made into an Inet6Address
+     * @param addrHexString a 32 character hex string representing an IPv6 addr
+     * @return addr an InetAddress representation for the string
+     */
+    public static InetAddress hexToInet6Address(String addrHexString)
+            throws IllegalArgumentException {
+        try {
+            return numericToInetAddress(String.format(Locale.US, "%s:%s:%s:%s:%s:%s:%s:%s",
+                    addrHexString.substring(0,4),   addrHexString.substring(4,8),
+                    addrHexString.substring(8,12),  addrHexString.substring(12,16),
+                    addrHexString.substring(16,20), addrHexString.substring(20,24),
+                    addrHexString.substring(24,28), addrHexString.substring(28,32)));
+        } catch (Exception e) {
+            Log.e("NetworkUtils", "error in hexToInet6Address(" + addrHexString + "): " + e);
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Trim leading zeros from IPv4 address strings
+     * Our base libraries will interpret that as octel..
+     * Must leave non v4 addresses and host names alone.
+     * For example, 192.168.000.010 -> 192.168.0.10
+     * TODO - fix base libraries and remove this function
+     * @param addr a string representing an ip addr
+     * @return a string propertly trimmed
+     */
+    @UnsupportedAppUsage
+    public static String trimV4AddrZeros(String addr) {
+        return Inet4AddressUtils.trimAddressZeros(addr);
+    }
+
+    /**
+     * Returns a prefix set without overlaps.
+     *
+     * This expects the src set to be sorted from shorter to longer. Results are undefined
+     * failing this condition. The returned prefix set is sorted in the same order as the
+     * passed set, with the same comparator.
+     */
+    private static TreeSet<IpPrefix> deduplicatePrefixSet(final TreeSet<IpPrefix> src) {
+        final TreeSet<IpPrefix> dst = new TreeSet<>(src.comparator());
+        // Prefixes match addresses that share their upper part up to their length, therefore
+        // the only kind of possible overlap in two prefixes is strict inclusion of the longer
+        // (more restrictive) in the shorter (including equivalence if they have the same
+        // length).
+        // Because prefixes in the src set are sorted from shorter to longer, deduplicating
+        // is done by simply iterating in order, and not adding any longer prefix that is
+        // already covered by a shorter one.
+        newPrefixes:
+        for (IpPrefix newPrefix : src) {
+            for (IpPrefix existingPrefix : dst) {
+                if (existingPrefix.containsPrefix(newPrefix)) {
+                    continue newPrefixes;
+                }
+            }
+            dst.add(newPrefix);
+        }
+        return dst;
+    }
+
+    /**
+     * Returns how many IPv4 addresses match any of the prefixes in the passed ordered set.
+     *
+     * Obviously this returns an integral value between 0 and 2**32.
+     * The behavior is undefined if any of the prefixes is not an IPv4 prefix or if the
+     * set is not ordered smallest prefix to longer prefix.
+     *
+     * @param prefixes the set of prefixes, ordered by length
+     */
+    public static long routedIPv4AddressCount(final TreeSet<IpPrefix> prefixes) {
+        long routedIPCount = 0;
+        for (final IpPrefix prefix : deduplicatePrefixSet(prefixes)) {
+            if (!prefix.isIPv4()) {
+                Log.wtf(TAG, "Non-IPv4 prefix in routedIPv4AddressCount");
+            }
+            int rank = 32 - prefix.getPrefixLength();
+            routedIPCount += 1L << rank;
+        }
+        return routedIPCount;
+    }
+
+    /**
+     * Returns how many IPv6 addresses match any of the prefixes in the passed ordered set.
+     *
+     * This returns a BigInteger between 0 and 2**128.
+     * The behavior is undefined if any of the prefixes is not an IPv6 prefix or if the
+     * set is not ordered smallest prefix to longer prefix.
+     */
+    public static BigInteger routedIPv6AddressCount(final TreeSet<IpPrefix> prefixes) {
+        BigInteger routedIPCount = BigInteger.ZERO;
+        for (final IpPrefix prefix : deduplicatePrefixSet(prefixes)) {
+            if (!prefix.isIPv6()) {
+                Log.wtf(TAG, "Non-IPv6 prefix in routedIPv6AddressCount");
+            }
+            int rank = 128 - prefix.getPrefixLength();
+            routedIPCount = routedIPCount.add(BigInteger.ONE.shiftLeft(rank));
+        }
+        return routedIPCount;
+    }
+
+}
diff --git a/framework/src/android/net/OemNetworkPreferences.java b/framework/src/android/net/OemNetworkPreferences.java
new file mode 100644
index 0000000..2bb006d
--- /dev/null
+++ b/framework/src/android/net/OemNetworkPreferences.java
@@ -0,0 +1,276 @@
+/*
+ * 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.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Network preferences to set the default active network on a per-application basis as per a given
+ * {@link OemNetworkPreference}. An example of this would be to set an application's network
+ * preference to {@link #OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK} which would have the default
+ * network for that application set to an unmetered network first if available and if not, it then
+ * set that application's default network to an OEM managed network if available.
+ *
+ * @hide
+ */
+@SystemApi
+public final class OemNetworkPreferences implements Parcelable {
+    // Valid production preferences must be > 0, negative values reserved for testing
+    /**
+     * This preference is only to be used for testing and nothing else.
+     * Use only TRANSPORT_TEST transport networks.
+     * @hide
+     */
+    public static final int OEM_NETWORK_PREFERENCE_TEST_ONLY = -2;
+
+    /**
+     * This preference is only to be used for testing and nothing else.
+     * If an unmetered network is available, use it.
+     * Otherwise, if a network with the TRANSPORT_TEST transport is available, use it.
+     * Otherwise, use the general default network.
+     * @hide
+     */
+    public static final int OEM_NETWORK_PREFERENCE_TEST = -1;
+
+    /**
+     * Default in case this value is not set. Using it will result in an error.
+     */
+    public static final int OEM_NETWORK_PREFERENCE_UNINITIALIZED = 0;
+
+    /**
+     * If an unmetered network is available, use it.
+     * Otherwise, if a network with the OEM_PAID capability is available, use it.
+     * Otherwise, use the general default network.
+     */
+    public static final int OEM_NETWORK_PREFERENCE_OEM_PAID = 1;
+
+    /**
+     * If an unmetered network is available, use it.
+     * Otherwise, if a network with the OEM_PAID capability is available, use it.
+     * Otherwise, the app doesn't get a default network.
+     */
+    public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK = 2;
+
+    /**
+     * Use only NET_CAPABILITY_OEM_PAID networks.
+     */
+    public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY = 3;
+
+    /**
+     * Use only NET_CAPABILITY_OEM_PRIVATE networks.
+     */
+    public static final int OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY = 4;
+
+    /**
+     * The max allowed value for an OEM network preference.
+     * @hide
+     */
+    public static final int OEM_NETWORK_PREFERENCE_MAX = OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY;
+
+    @NonNull
+    private final Bundle mNetworkMappings;
+
+    /**
+     * Return whether this object is empty.
+     * @hide
+     */
+    public boolean isEmpty() {
+        return mNetworkMappings.keySet().size() == 0;
+    }
+
+    /**
+     * Return the currently built application package name to {@link OemNetworkPreference} mappings.
+     * @return the current network preferences map.
+     */
+    @NonNull
+    public Map<String, Integer> getNetworkPreferences() {
+        return convertToUnmodifiableMap(mNetworkMappings);
+    }
+
+    private OemNetworkPreferences(@NonNull final Bundle networkMappings) {
+        Objects.requireNonNull(networkMappings);
+        mNetworkMappings = (Bundle) networkMappings.clone();
+    }
+
+    @Override
+    public String toString() {
+        return "OemNetworkPreferences{" + "mNetworkMappings=" + getNetworkPreferences() + '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        OemNetworkPreferences that = (OemNetworkPreferences) o;
+
+        return mNetworkMappings.size() == that.mNetworkMappings.size()
+                && mNetworkMappings.toString().equals(that.mNetworkMappings.toString());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mNetworkMappings);
+    }
+
+    /**
+     * Builder used to create {@link OemNetworkPreferences} objects.  Specify the preferred Network
+     * to package name mappings.
+     */
+    public static final class Builder {
+        private final Bundle mNetworkMappings;
+
+        public Builder() {
+            mNetworkMappings = new Bundle();
+        }
+
+        /**
+         * Constructor to populate the builder's values with an already built
+         * {@link OemNetworkPreferences}.
+         * @param preferences the {@link OemNetworkPreferences} to populate with.
+         */
+        public Builder(@NonNull final OemNetworkPreferences preferences) {
+            Objects.requireNonNull(preferences);
+            mNetworkMappings = (Bundle) preferences.mNetworkMappings.clone();
+        }
+
+        /**
+         * Add a network preference for a given package. Previously stored values for the given
+         * package will be overwritten.
+         *
+         * @param packageName full package name (e.g.: "com.google.apps.contacts") of the app
+         *                    to use the given preference
+         * @param preference  the desired network preference to use
+         * @return The builder to facilitate chaining.
+         */
+        @NonNull
+        public Builder addNetworkPreference(@NonNull final String packageName,
+                @OemNetworkPreference final int preference) {
+            Objects.requireNonNull(packageName);
+            mNetworkMappings.putInt(packageName, preference);
+            return this;
+        }
+
+        /**
+         * Remove a network preference for a given package.
+         *
+         * @param packageName full package name (e.g.: "com.google.apps.contacts") of the app to
+         *                    remove a preference for.
+         * @return The builder to facilitate chaining.
+         */
+        @NonNull
+        public Builder clearNetworkPreference(@NonNull final String packageName) {
+            Objects.requireNonNull(packageName);
+            mNetworkMappings.remove(packageName);
+            return this;
+        }
+
+        /**
+         * Build {@link OemNetworkPreferences} return the current OEM network preferences.
+         */
+        @NonNull
+        public OemNetworkPreferences build() {
+            return new OemNetworkPreferences(mNetworkMappings);
+        }
+    }
+
+    private static Map<String, Integer> convertToUnmodifiableMap(@NonNull final Bundle bundle) {
+        final Map<String, Integer> networkPreferences = new HashMap<>();
+        for (final String key : bundle.keySet()) {
+            networkPreferences.put(key, bundle.getInt(key));
+        }
+        return Collections.unmodifiableMap(networkPreferences);
+    }
+
+    /** @hide */
+    @IntDef(prefix = "OEM_NETWORK_PREFERENCE_", value = {
+            OEM_NETWORK_PREFERENCE_TEST_ONLY,
+            OEM_NETWORK_PREFERENCE_TEST,
+            OEM_NETWORK_PREFERENCE_UNINITIALIZED,
+            OEM_NETWORK_PREFERENCE_OEM_PAID,
+            OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK,
+            OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY,
+            OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface OemNetworkPreference {}
+
+    /**
+     * Return the string value for OemNetworkPreference
+     *
+     * @param value int value of OemNetworkPreference
+     * @return string version of OemNetworkPreference
+     *
+     * @hide
+     */
+    @NonNull
+    public static String oemNetworkPreferenceToString(@OemNetworkPreference int value) {
+        switch (value) {
+            case OEM_NETWORK_PREFERENCE_TEST_ONLY:
+                return "OEM_NETWORK_PREFERENCE_TEST_ONLY";
+            case OEM_NETWORK_PREFERENCE_TEST:
+                return "OEM_NETWORK_PREFERENCE_TEST";
+            case OEM_NETWORK_PREFERENCE_UNINITIALIZED:
+                return "OEM_NETWORK_PREFERENCE_UNINITIALIZED";
+            case OEM_NETWORK_PREFERENCE_OEM_PAID:
+                return "OEM_NETWORK_PREFERENCE_OEM_PAID";
+            case OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK:
+                return "OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK";
+            case OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY:
+                return "OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY";
+            case OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY:
+                return "OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY";
+            default:
+                return Integer.toHexString(value);
+        }
+    }
+
+    @Override
+    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+        dest.writeBundle(mNetworkMappings);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<OemNetworkPreferences> CREATOR =
+            new Parcelable.Creator<OemNetworkPreferences>() {
+                @Override
+                public OemNetworkPreferences[] newArray(int size) {
+                    return new OemNetworkPreferences[size];
+                }
+
+                @Override
+                public OemNetworkPreferences createFromParcel(@NonNull android.os.Parcel in) {
+                    return new OemNetworkPreferences(
+                            in.readBundle(getClass().getClassLoader()));
+                }
+            };
+}
diff --git a/framework/src/android/net/ParseException.java b/framework/src/android/net/ParseException.java
new file mode 100644
index 0000000..9d4727a
--- /dev/null
+++ b/framework/src/android/net/ParseException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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;
+
+/**
+ * Thrown when parsing failed.
+ */
+// See non-public class {@link WebAddress}.
+public class ParseException extends RuntimeException {
+    public String response;
+
+    public ParseException(@NonNull String response) {
+        super(response);
+        this.response = response;
+    }
+
+    public ParseException(@NonNull String response, @NonNull Throwable cause) {
+        super(response, cause);
+        this.response = response;
+    }
+}
diff --git a/framework/src/android/net/ProxyInfo.java b/framework/src/android/net/ProxyInfo.java
new file mode 100644
index 0000000..745e20f
--- /dev/null
+++ b/framework/src/android/net/ProxyInfo.java
@@ -0,0 +1,369 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import com.android.net.module.util.ProxyUtils;
+
+import java.net.InetSocketAddress;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Describes a proxy configuration.
+ *
+ * Proxy configurations are already integrated within the {@code java.net} and
+ * Apache HTTP stack. So {@link URLConnection} and Apache's {@code HttpClient} will use
+ * them automatically.
+ *
+ * Other HTTP stacks will need to obtain the proxy info from
+ * {@link Proxy#PROXY_CHANGE_ACTION} broadcast as the extra {@link Proxy#EXTRA_PROXY_INFO}.
+ */
+public class ProxyInfo implements Parcelable {
+
+    private final String mHost;
+    private final int mPort;
+    private final String mExclusionList;
+    private final String[] mParsedExclusionList;
+    private final Uri mPacFileUrl;
+
+    /**
+     *@hide
+     */
+    public static final String LOCAL_EXCL_LIST = "";
+    /**
+     *@hide
+     */
+    public static final int LOCAL_PORT = -1;
+    /**
+     *@hide
+     */
+    public static final String LOCAL_HOST = "localhost";
+
+    /**
+     * Constructs a {@link ProxyInfo} object that points at a Direct proxy
+     * on the specified host and port.
+     */
+    public static ProxyInfo buildDirectProxy(String host, int port) {
+        return new ProxyInfo(host, port, null);
+    }
+
+    /**
+     * Constructs a {@link ProxyInfo} object that points at a Direct proxy
+     * on the specified host and port.
+     *
+     * The proxy will not be used to access any host in exclusion list, exclList.
+     *
+     * @param exclList Hosts to exclude using the proxy on connections for.  These
+     *                 hosts can use wildcards such as *.example.com.
+     */
+    public static ProxyInfo buildDirectProxy(String host, int port, List<String> exclList) {
+        String[] array = exclList.toArray(new String[exclList.size()]);
+        return new ProxyInfo(host, port, TextUtils.join(",", array), array);
+    }
+
+    /**
+     * Construct a {@link ProxyInfo} that will download and run the PAC script
+     * at the specified URL.
+     */
+    public static ProxyInfo buildPacProxy(Uri pacUri) {
+        return new ProxyInfo(pacUri);
+    }
+
+    /**
+     * Construct a {@link ProxyInfo} object that will download and run the PAC script at the
+     * specified URL and port.
+     */
+    @NonNull
+    public static ProxyInfo buildPacProxy(@NonNull Uri pacUrl, int port) {
+        return new ProxyInfo(pacUrl, port);
+    }
+
+    /**
+     * Create a ProxyProperties that points at a HTTP Proxy.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public ProxyInfo(String host, int port, String exclList) {
+        mHost = host;
+        mPort = port;
+        mExclusionList = exclList;
+        mParsedExclusionList = parseExclusionList(mExclusionList);
+        mPacFileUrl = Uri.EMPTY;
+    }
+
+    /**
+     * Create a ProxyProperties that points at a PAC URL.
+     * @hide
+     */
+    public ProxyInfo(@NonNull Uri pacFileUrl) {
+        mHost = LOCAL_HOST;
+        mPort = LOCAL_PORT;
+        mExclusionList = LOCAL_EXCL_LIST;
+        mParsedExclusionList = parseExclusionList(mExclusionList);
+        if (pacFileUrl == null) {
+            throw new NullPointerException();
+        }
+        mPacFileUrl = pacFileUrl;
+    }
+
+    /**
+     * Only used in PacProxyService after Local Proxy is bound.
+     * @hide
+     */
+    public ProxyInfo(@NonNull Uri pacFileUrl, int localProxyPort) {
+        mHost = LOCAL_HOST;
+        mPort = localProxyPort;
+        mExclusionList = LOCAL_EXCL_LIST;
+        mParsedExclusionList = parseExclusionList(mExclusionList);
+        if (pacFileUrl == null) {
+            throw new NullPointerException();
+        }
+        mPacFileUrl = pacFileUrl;
+    }
+
+    private static String[] parseExclusionList(String exclusionList) {
+        if (exclusionList == null) {
+            return new String[0];
+        } else {
+            return exclusionList.toLowerCase(Locale.ROOT).split(",");
+        }
+    }
+
+    private ProxyInfo(String host, int port, String exclList, String[] parsedExclList) {
+        mHost = host;
+        mPort = port;
+        mExclusionList = exclList;
+        mParsedExclusionList = parsedExclList;
+        mPacFileUrl = Uri.EMPTY;
+    }
+
+    /**
+     * A copy constructor to hold proxy properties.
+     */
+    public ProxyInfo(@Nullable ProxyInfo source) {
+        if (source != null) {
+            mHost = source.getHost();
+            mPort = source.getPort();
+            mPacFileUrl = source.mPacFileUrl;
+            mExclusionList = source.getExclusionListAsString();
+            mParsedExclusionList = source.mParsedExclusionList;
+        } else {
+            mHost = null;
+            mPort = 0;
+            mExclusionList = null;
+            mParsedExclusionList = null;
+            mPacFileUrl = Uri.EMPTY;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public InetSocketAddress getSocketAddress() {
+        InetSocketAddress inetSocketAddress = null;
+        try {
+            inetSocketAddress = new InetSocketAddress(mHost, mPort);
+        } catch (IllegalArgumentException e) { }
+        return inetSocketAddress;
+    }
+
+    /**
+     * Returns the URL of the current PAC script or null if there is
+     * no PAC script.
+     */
+    public Uri getPacFileUrl() {
+        return mPacFileUrl;
+    }
+
+    /**
+     * When configured to use a Direct Proxy this returns the host
+     * of the proxy.
+     */
+    public String getHost() {
+        return mHost;
+    }
+
+    /**
+     * When configured to use a Direct Proxy this returns the port
+     * of the proxy
+     */
+    public int getPort() {
+        return mPort;
+    }
+
+    /**
+     * When configured to use a Direct Proxy this returns the list
+     * of hosts for which the proxy is ignored.
+     */
+    public String[] getExclusionList() {
+        return mParsedExclusionList;
+    }
+
+    /**
+     * comma separated
+     * @hide
+     */
+    @Nullable
+    public String getExclusionListAsString() {
+        return mExclusionList;
+    }
+
+    /**
+     * Return true if the pattern of proxy is valid, otherwise return false.
+     */
+    public boolean isValid() {
+        if (!Uri.EMPTY.equals(mPacFileUrl)) return true;
+        return ProxyUtils.PROXY_VALID == ProxyUtils.validate(mHost == null ? "" : mHost,
+                mPort == 0 ? "" : Integer.toString(mPort),
+                mExclusionList == null ? "" : mExclusionList);
+    }
+
+    /**
+     * @hide
+     */
+    public java.net.Proxy makeProxy() {
+        java.net.Proxy proxy = java.net.Proxy.NO_PROXY;
+        if (mHost != null) {
+            try {
+                InetSocketAddress inetSocketAddress = new InetSocketAddress(mHost, mPort);
+                proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, inetSocketAddress);
+            } catch (IllegalArgumentException e) {
+            }
+        }
+        return proxy;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        if (!Uri.EMPTY.equals(mPacFileUrl)) {
+            sb.append("PAC Script: ");
+            sb.append(mPacFileUrl);
+        }
+        if (mHost != null) {
+            sb.append("[");
+            sb.append(mHost);
+            sb.append("] ");
+            sb.append(Integer.toString(mPort));
+            if (mExclusionList != null) {
+                sb.append(" xl=").append(mExclusionList);
+            }
+        } else {
+            sb.append("[ProxyProperties.mHost == null]");
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (!(o instanceof ProxyInfo)) return false;
+        ProxyInfo p = (ProxyInfo)o;
+        // If PAC URL is present in either then they must be equal.
+        // Other parameters will only be for fall back.
+        if (!Uri.EMPTY.equals(mPacFileUrl)) {
+            return mPacFileUrl.equals(p.getPacFileUrl()) && mPort == p.mPort;
+        }
+        if (!Uri.EMPTY.equals(p.mPacFileUrl)) {
+            return false;
+        }
+        if (mExclusionList != null && !mExclusionList.equals(p.getExclusionListAsString())) {
+            return false;
+        }
+        if (mHost != null && p.getHost() != null && mHost.equals(p.getHost()) == false) {
+            return false;
+        }
+        if (mHost != null && p.mHost == null) return false;
+        if (mHost == null && p.mHost != null) return false;
+        if (mPort != p.mPort) return false;
+        return true;
+    }
+
+    /**
+     * Implement the Parcelable interface
+     * @hide
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    /*
+     * generate hashcode based on significant fields
+     */
+    public int hashCode() {
+        return ((null == mHost) ? 0 : mHost.hashCode())
+                + ((null == mExclusionList) ? 0 : mExclusionList.hashCode())
+                + mPort;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        if (!Uri.EMPTY.equals(mPacFileUrl)) {
+            dest.writeByte((byte)1);
+            mPacFileUrl.writeToParcel(dest, 0);
+            dest.writeInt(mPort);
+            return;
+        } else {
+            dest.writeByte((byte)0);
+        }
+        if (mHost != null) {
+            dest.writeByte((byte)1);
+            dest.writeString(mHost);
+            dest.writeInt(mPort);
+        } else {
+            dest.writeByte((byte)0);
+        }
+        dest.writeString(mExclusionList);
+        dest.writeStringArray(mParsedExclusionList);
+    }
+
+    public static final @android.annotation.NonNull Creator<ProxyInfo> CREATOR =
+        new Creator<ProxyInfo>() {
+            public ProxyInfo createFromParcel(Parcel in) {
+                String host = null;
+                int port = 0;
+                if (in.readByte() != 0) {
+                    Uri url = Uri.CREATOR.createFromParcel(in);
+                    int localPort = in.readInt();
+                    return new ProxyInfo(url, localPort);
+                }
+                if (in.readByte() != 0) {
+                    host = in.readString();
+                    port = in.readInt();
+                }
+                String exclList = in.readString();
+                String[] parsedExclList = in.createStringArray();
+                ProxyInfo proxyProperties = new ProxyInfo(host, port, exclList, parsedExclList);
+                return proxyProperties;
+            }
+
+            public ProxyInfo[] newArray(int size) {
+                return new ProxyInfo[size];
+            }
+        };
+}
diff --git a/framework/src/android/net/QosCallback.java b/framework/src/android/net/QosCallback.java
new file mode 100644
index 0000000..22f06bc
--- /dev/null
+++ b/framework/src/android/net/QosCallback.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Receives Qos information given a {@link Network}.  The callback is registered with
+ * {@link ConnectivityManager#registerQosCallback}.
+ *
+ * <p>
+ * <br/>
+ * The callback will no longer receive calls if any of the following takes place:
+ * <ol>
+ * <li>{@link ConnectivityManager#unregisterQosCallback(QosCallback)} is called with the same
+ * callback instance.</li>
+ * <li>{@link QosCallback#onError(QosCallbackException)} is called.</li>
+ * <li>A network specific issue occurs.  eg. Congestion on a carrier network.</li>
+ * <li>The network registered with the callback has no associated QoS providers</li>
+ * </ul>
+ * {@hide}
+ */
+@SystemApi
+public abstract class QosCallback {
+    /**
+     * Invoked after an error occurs on a registered callback.  Once called, the callback is
+     * automatically unregistered and the callback will no longer receive calls.
+     *
+     * <p>The underlying exception can either be a runtime exception or a custom exception made for
+     * {@link QosCallback}. see: {@link QosCallbackException}.
+     *
+     * @param exception wraps the underlying cause
+     */
+    public void onError(@NonNull final QosCallbackException exception) {
+    }
+
+    /**
+     * Called when a Qos Session first becomes available to the callback or if its attributes have
+     * changed.
+     * <p>
+     * Note: The callback may be called multiple times with the same attributes.
+     *
+     * @param session the available session
+     * @param sessionAttributes the attributes of the session
+     */
+    public void onQosSessionAvailable(@NonNull final QosSession session,
+            @NonNull final QosSessionAttributes sessionAttributes) {
+    }
+
+    /**
+     * Called after a Qos Session is lost.
+     * <p>
+     * At least one call to
+     * {@link QosCallback#onQosSessionAvailable(QosSession, QosSessionAttributes)}
+     * with the same {@link QosSession} will precede a call to lost.
+     *
+     * @param session the lost session
+     */
+    public void onQosSessionLost(@NonNull final QosSession session) {
+    }
+
+    /**
+     * Thrown when there is a problem registering {@link QosCallback} with
+     * {@link ConnectivityManager#registerQosCallback(QosSocketInfo, QosCallback, Executor)}.
+     */
+    public static class QosCallbackRegistrationException extends RuntimeException {
+        /**
+         * @hide
+         */
+        public QosCallbackRegistrationException() {
+            super();
+        }
+    }
+}
diff --git a/framework/src/android/net/QosCallbackConnection.java b/framework/src/android/net/QosCallbackConnection.java
new file mode 100644
index 0000000..de0fc24
--- /dev/null
+++ b/framework/src/android/net/QosCallbackConnection.java
@@ -0,0 +1,148 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.telephony.data.EpsBearerQosSessionAttributes;
+import android.telephony.data.NrQosSessionAttributes;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Sends messages from {@link com.android.server.ConnectivityService} to the registered
+ * {@link QosCallback}.
+ * <p/>
+ * This is a satellite class of {@link ConnectivityManager} and not meant
+ * to be used in other contexts.
+ *
+ * @hide
+ */
+class QosCallbackConnection extends android.net.IQosCallback.Stub {
+
+    @NonNull private final ConnectivityManager mConnectivityManager;
+    @Nullable private volatile QosCallback mCallback;
+    @NonNull private final Executor mExecutor;
+
+    @VisibleForTesting
+    @Nullable
+    public QosCallback getCallback() {
+        return mCallback;
+    }
+
+    /**
+     * The constructor for the connection
+     *
+     * @param connectivityManager the mgr that created this connection
+     * @param callback the callback to send messages back to
+     * @param executor The executor on which the callback will be invoked. The provided
+     *                 {@link Executor} must run callback sequentially, otherwise the order of
+     *                 callbacks cannot be guaranteed.
+     */
+    QosCallbackConnection(@NonNull final ConnectivityManager connectivityManager,
+            @NonNull final QosCallback callback,
+            @NonNull final Executor executor) {
+        mConnectivityManager = Objects.requireNonNull(connectivityManager,
+                "connectivityManager must be non-null");
+        mCallback = Objects.requireNonNull(callback, "callback must be non-null");
+        mExecutor = Objects.requireNonNull(executor, "executor must be non-null");
+    }
+
+    /**
+     * Called when either the {@link EpsBearerQosSessionAttributes} has changed or on the first time
+     * the attributes have become available.
+     *
+     * @param session the session that is now available
+     * @param attributes the corresponding attributes of session
+     */
+    @Override
+    public void onQosEpsBearerSessionAvailable(@NonNull final QosSession session,
+            @NonNull final EpsBearerQosSessionAttributes attributes) {
+
+        mExecutor.execute(() -> {
+            final QosCallback callback = mCallback;
+            if (callback != null) {
+                callback.onQosSessionAvailable(session, attributes);
+            }
+        });
+    }
+
+    /**
+     * Called when either the {@link NrQosSessionAttributes} has changed or on the first time
+     * the attributes have become available.
+     *
+     * @param session the session that is now available
+     * @param attributes the corresponding attributes of session
+     */
+    @Override
+    public void onNrQosSessionAvailable(@NonNull final QosSession session,
+            @NonNull final NrQosSessionAttributes attributes) {
+
+        mExecutor.execute(() -> {
+            final QosCallback callback = mCallback;
+            if (callback != null) {
+                callback.onQosSessionAvailable(session, attributes);
+            }
+        });
+    }
+
+    /**
+     * Called when the session is lost.
+     *
+     * @param session the session that was lost
+     */
+    @Override
+    public void onQosSessionLost(@NonNull final QosSession session) {
+        mExecutor.execute(() -> {
+            final QosCallback callback = mCallback;
+            if (callback != null) {
+                callback.onQosSessionLost(session);
+            }
+        });
+    }
+
+    /**
+     * Called when there is an error on the registered callback.
+     *
+     *  @param errorType the type of error
+     */
+    @Override
+    public void onError(@QosCallbackException.ExceptionType final int errorType) {
+        mExecutor.execute(() -> {
+            final QosCallback callback = mCallback;
+            if (callback != null) {
+                // Messages no longer need to be received since there was an error.
+                stopReceivingMessages();
+                mConnectivityManager.unregisterQosCallback(callback);
+                callback.onError(QosCallbackException.createException(errorType));
+            }
+        });
+    }
+
+    /**
+     * The callback will stop receiving messages.
+     * <p/>
+     * There are no synchronization guarantees on exactly when the callback will stop receiving
+     * messages.
+     */
+    void stopReceivingMessages() {
+        mCallback = null;
+    }
+}
diff --git a/framework/src/android/net/QosCallbackException.java b/framework/src/android/net/QosCallbackException.java
new file mode 100644
index 0000000..7fd9a52
--- /dev/null
+++ b/framework/src/android/net/QosCallbackException.java
@@ -0,0 +1,110 @@
+/*
+ * 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;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This is the exception type passed back through the onError method on {@link QosCallback}.
+ * {@link QosCallbackException#getCause()} contains the actual error that caused this exception.
+ *
+ * The possible exception types as causes are:
+ * 1. {@link NetworkReleasedException}
+ * 2. {@link SocketNotBoundException}
+ * 3. {@link UnsupportedOperationException}
+ * 4. {@link SocketLocalAddressChangedException}
+ *
+ * @hide
+ */
+@SystemApi
+public final class QosCallbackException extends Exception {
+
+    /** @hide */
+    @IntDef(prefix = {"EX_TYPE_"}, value = {
+            EX_TYPE_FILTER_NONE,
+            EX_TYPE_FILTER_NETWORK_RELEASED,
+            EX_TYPE_FILTER_SOCKET_NOT_BOUND,
+            EX_TYPE_FILTER_NOT_SUPPORTED,
+            EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ExceptionType {}
+
+    private static final String TAG = "QosCallbackException";
+
+    // Types of exceptions supported //
+    /** {@hide} */
+    public static final int EX_TYPE_FILTER_NONE = 0;
+
+    /** {@hide} */
+    public static final int EX_TYPE_FILTER_NETWORK_RELEASED = 1;
+
+    /** {@hide} */
+    public static final int EX_TYPE_FILTER_SOCKET_NOT_BOUND = 2;
+
+    /** {@hide} */
+    public static final int EX_TYPE_FILTER_NOT_SUPPORTED = 3;
+
+    /** {@hide} */
+    public static final int EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED = 4;
+
+    /**
+     * Creates exception based off of a type and message.  Not all types of exceptions accept a
+     * custom message.
+     *
+     * {@hide}
+     */
+    @NonNull
+    static QosCallbackException createException(@ExceptionType final int type) {
+        switch (type) {
+            case EX_TYPE_FILTER_NETWORK_RELEASED:
+                return new QosCallbackException(new NetworkReleasedException());
+            case EX_TYPE_FILTER_SOCKET_NOT_BOUND:
+                return new QosCallbackException(new SocketNotBoundException());
+            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());
+            default:
+                Log.wtf(TAG, "create: No case setup for exception type: '" + type + "'");
+                return new QosCallbackException(
+                        new RuntimeException("Unknown exception code: " + type));
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public QosCallbackException(@NonNull final String message) {
+        super(message);
+    }
+
+    /**
+     * @hide
+     */
+    public QosCallbackException(@NonNull final Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/framework/src/android/net/QosFilter.java b/framework/src/android/net/QosFilter.java
new file mode 100644
index 0000000..957c867
--- /dev/null
+++ b/framework/src/android/net/QosFilter.java
@@ -0,0 +1,86 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import java.net.InetAddress;
+
+/**
+ * Provides the related filtering logic to the {@link NetworkAgent} to match {@link QosSession}s
+ * to their related {@link QosCallback}.
+ *
+ * Used by the {@link com.android.server.ConnectivityService} to validate a {@link QosCallback}
+ * is still able to receive a {@link QosSession}.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class QosFilter {
+
+    /**
+     * The constructor is kept hidden from outside this package to ensure that all derived types
+     * are known and properly handled when being passed to and from {@link NetworkAgent}.
+     *
+     * @hide
+     */
+    QosFilter() {
+    }
+
+    /**
+     * The network used with this filter.
+     *
+     * @return the registered {@link Network}
+     */
+    @NonNull
+    public abstract Network getNetwork();
+
+    /**
+     * Validates that conditions have not changed such that no further {@link QosSession}s should
+     * be passed back to the {@link QosCallback} associated to this filter.
+     *
+     * @return the error code when present, otherwise the filter is valid
+     *
+     * @hide
+     */
+    @QosCallbackException.ExceptionType
+    public abstract int validate();
+
+    /**
+     * Determines whether or not the parameters is a match for the filter.
+     *
+     * @param address the local address
+     * @param startPort the start of the port range
+     * @param endPort the end of the port range
+     * @return whether the parameters match the local address of the filter
+     */
+    public abstract boolean matchesLocalAddress(@NonNull InetAddress address,
+            int startPort, int endPort);
+
+    /**
+     * Determines whether or not the parameters is a match for the filter.
+     *
+     * @param address the remote address
+     * @param startPort the start of the port range
+     * @param endPort the end of the port range
+     * @return whether the parameters match the remote address of the filter
+     */
+    public abstract boolean matchesRemoteAddress(@NonNull InetAddress address,
+            int startPort, int endPort);
+}
+
diff --git a/framework/src/android/net/QosFilterParcelable.java b/framework/src/android/net/QosFilterParcelable.java
new file mode 100644
index 0000000..da3b2cf
--- /dev/null
+++ b/framework/src/android/net/QosFilterParcelable.java
@@ -0,0 +1,113 @@
+/*
+ * 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.util.Log;
+
+import java.util.Objects;
+
+/**
+ * Aware of how to parcel different types of {@link QosFilter}s.  Any new type of qos filter must
+ * have a specialized case written here.
+ * <p/>
+ * Specifically leveraged when transferring {@link QosFilter} from
+ * {@link com.android.server.ConnectivityService} to {@link NetworkAgent} when the filter is first
+ * registered.
+ * <p/>
+ * This is not meant to be used in other contexts.
+ *
+ * @hide
+ */
+public final class QosFilterParcelable implements Parcelable {
+
+    private static final String LOG_TAG = QosFilterParcelable.class.getSimpleName();
+
+    // Indicates that the filter was not successfully written to the parcel.
+    private static final int NO_FILTER_PRESENT = 0;
+
+    // The parcel is of type qos socket filter.
+    private static final int QOS_SOCKET_FILTER = 1;
+
+    private final QosFilter mQosFilter;
+
+    /**
+     * The underlying qos filter.
+     * <p/>
+     * Null only in the case parceling failed.
+     */
+    @Nullable
+    public QosFilter getQosFilter() {
+        return mQosFilter;
+    }
+
+    public QosFilterParcelable(@NonNull final QosFilter qosFilter) {
+        Objects.requireNonNull(qosFilter, "qosFilter must be non-null");
+
+        // NOTE: Normally a type check would belong here, but doing so breaks unit tests that rely
+        // on mocking qos filter.
+        mQosFilter = qosFilter;
+    }
+
+    private QosFilterParcelable(final Parcel in) {
+        final int filterParcelType = in.readInt();
+
+        switch (filterParcelType) {
+            case QOS_SOCKET_FILTER: {
+                mQosFilter = new QosSocketFilter(QosSocketInfo.CREATOR.createFromParcel(in));
+                break;
+            }
+
+            case NO_FILTER_PRESENT:
+            default: {
+                mQosFilter = null;
+            }
+        }
+    }
+
+    public static final Creator<QosFilterParcelable> CREATOR = new Creator<QosFilterParcelable>() {
+        @Override
+        public QosFilterParcelable createFromParcel(final Parcel in) {
+            return new QosFilterParcelable(in);
+        }
+
+        @Override
+        public QosFilterParcelable[] newArray(final int size) {
+            return new QosFilterParcelable[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(final Parcel dest, final int flags) {
+        if (mQosFilter instanceof QosSocketFilter) {
+            dest.writeInt(QOS_SOCKET_FILTER);
+            final QosSocketFilter qosSocketFilter = (QosSocketFilter) mQosFilter;
+            qosSocketFilter.getQosSocketInfo().writeToParcel(dest, 0);
+            return;
+        }
+        dest.writeInt(NO_FILTER_PRESENT);
+        Log.e(LOG_TAG, "Parceling failed, unknown type of filter present: " + mQosFilter);
+    }
+}
diff --git a/framework/src/android/net/QosSession.java b/framework/src/android/net/QosSession.java
new file mode 100644
index 0000000..93f2ff2
--- /dev/null
+++ b/framework/src/android/net/QosSession.java
@@ -0,0 +1,142 @@
+/*
+ * 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;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Provides identifying information of a QoS session.  Sent to an application through
+ * {@link QosCallback}.
+ *
+ * @hide
+ */
+@SystemApi
+public final class QosSession implements Parcelable {
+
+    /**
+     * The {@link QosSession} is a LTE EPS Session.
+     */
+    public static final int TYPE_EPS_BEARER = 1;
+
+    /**
+     * The {@link QosSession} is a NR Session.
+     */
+    public static final int TYPE_NR_BEARER = 2;
+
+    private final int mSessionId;
+
+    private final int mSessionType;
+
+    /**
+     * Gets the unique id of the session that is used to differentiate sessions across different
+     * types.
+     * <p/>
+     * Note: Different qos sessions can be provided by different actors.
+     *
+     * @return the unique id
+     */
+    public long getUniqueId() {
+        return (long) mSessionType << 32 | mSessionId;
+    }
+
+    /**
+     * Gets the session id that is unique within that type.
+     * <p/>
+     * Note: The session id is set by the actor providing the qos.  It can be either manufactured by
+     * the actor, but also may have a particular meaning within that type.  For example, using the
+     * bearer id as the session id for {@link android.telephony.data.EpsBearerQosSessionAttributes}
+     * is a straight forward way to keep the sessions unique from one another within that type.
+     *
+     * @return the id of the session
+     */
+    public int getSessionId() {
+        return mSessionId;
+    }
+
+    /**
+     * Gets the type of session.
+     */
+    @QosSessionType
+    public int getSessionType() {
+        return mSessionType;
+    }
+
+    /**
+     * Creates a {@link QosSession}.
+     *
+     * @param sessionId uniquely identifies the session across all sessions of the same type
+     * @param sessionType the type of session
+     */
+    public QosSession(final int sessionId, @QosSessionType final int sessionType) {
+        //Ensures the session id is unique across types of sessions
+        mSessionId = sessionId;
+        mSessionType = sessionType;
+    }
+
+
+    @Override
+    public String toString() {
+        return "QosSession{"
+                + "mSessionId=" + mSessionId
+                + ", mSessionType=" + mSessionType
+                + '}';
+    }
+
+    /**
+     * Annotations for types of qos sessions.
+     */
+    @IntDef(value = {
+            TYPE_EPS_BEARER,
+            TYPE_NR_BEARER,
+    })
+    @interface QosSessionType {}
+
+    private QosSession(final Parcel in) {
+        mSessionId = in.readInt();
+        mSessionType = in.readInt();
+    }
+
+    @NonNull
+    public static final Creator<QosSession> CREATOR = new Creator<QosSession>() {
+        @NonNull
+        @Override
+        public QosSession createFromParcel(@NonNull final Parcel in) {
+            return new QosSession(in);
+        }
+
+        @NonNull
+        @Override
+        public QosSession[] newArray(final int size) {
+            return new QosSession[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull final Parcel dest, final int flags) {
+        dest.writeInt(mSessionId);
+        dest.writeInt(mSessionType);
+    }
+}
diff --git a/framework/src/android/net/QosSessionAttributes.java b/framework/src/android/net/QosSessionAttributes.java
new file mode 100644
index 0000000..7a88594
--- /dev/null
+++ b/framework/src/android/net/QosSessionAttributes.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+import android.annotation.SystemApi;
+
+/**
+ * Implemented by classes that encapsulate Qos related attributes that describe a Qos Session.
+ *
+ * Use the instanceof keyword to determine the underlying type.
+ *
+ * @hide
+ */
+@SystemApi
+public interface QosSessionAttributes {
+}
diff --git a/framework/src/android/net/QosSocketFilter.java b/framework/src/android/net/QosSocketFilter.java
new file mode 100644
index 0000000..69da7f4
--- /dev/null
+++ b/framework/src/android/net/QosSocketFilter.java
@@ -0,0 +1,179 @@
+/*
+ * 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;
+
+import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.util.Objects;
+
+/**
+ * Filters a {@link QosSession} according to the binding on the provided {@link Socket}.
+ *
+ * @hide
+ */
+public class QosSocketFilter extends QosFilter {
+
+    private static final String TAG = QosSocketFilter.class.getSimpleName();
+
+    @NonNull
+    private final QosSocketInfo mQosSocketInfo;
+
+    /**
+     * Creates a {@link QosSocketFilter} based off of {@link QosSocketInfo}.
+     *
+     * @param qosSocketInfo the information required to filter and validate
+     */
+    public QosSocketFilter(@NonNull final QosSocketInfo qosSocketInfo) {
+        Objects.requireNonNull(qosSocketInfo, "qosSocketInfo must be non-null");
+        mQosSocketInfo = qosSocketInfo;
+    }
+
+    /**
+     * Gets the parcelable qos socket info that was used to create the filter.
+     */
+    @NonNull
+    public QosSocketInfo getQosSocketInfo() {
+        return mQosSocketInfo;
+    }
+
+    /**
+     * Performs two validations:
+     * 1. If the socket is not bound, then return
+     *    {@link QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND}. This is detected
+     *    by checking the local address on the filter which becomes null when the socket is no
+     *    longer bound.
+     * 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.
+     * @return validation error code
+     */
+    @Override
+    public int validate() {
+        final InetSocketAddress sa = getAddressFromFileDescriptor();
+        if (sa == null) {
+            return QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+        }
+
+        if (!sa.equals(mQosSocketInfo.getLocalSocketAddress())) {
+            return EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
+        }
+
+        return EX_TYPE_FILTER_NONE;
+    }
+
+    /**
+     * The local address of the socket's binding.
+     *
+     * Note: If the socket is no longer bound, null is returned.
+     *
+     * @return the local address
+     */
+    @Nullable
+    private InetSocketAddress getAddressFromFileDescriptor() {
+        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) {
+            Log.e(TAG, "getAddressFromFileDescriptor: getLocalAddress exception", e);
+            return null;
+        }
+        if (address instanceof InetSocketAddress) {
+            return (InetSocketAddress) address;
+        }
+        return null;
+    }
+
+    /**
+     * The network used with this filter.
+     *
+     * @return the registered {@link Network}
+     */
+    @NonNull
+    @Override
+    public Network getNetwork() {
+        return mQosSocketInfo.getNetwork();
+    }
+
+    /**
+     * @inheritDoc
+     */
+    @Override
+    public boolean matchesLocalAddress(@NonNull final InetAddress address, final int startPort,
+            final int endPort) {
+        if (mQosSocketInfo.getLocalSocketAddress() == null) {
+            return false;
+        }
+        return matchesAddress(mQosSocketInfo.getLocalSocketAddress(), address, startPort,
+                endPort);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    @Override
+    public boolean matchesRemoteAddress(@NonNull final InetAddress address, final int startPort,
+            final int endPort) {
+        if (mQosSocketInfo.getRemoteSocketAddress() == null) {
+            return false;
+        }
+        return matchesAddress(mQosSocketInfo.getRemoteSocketAddress(), address, startPort,
+                endPort);
+    }
+
+    /**
+     * Called from {@link QosSocketFilter#matchesLocalAddress(InetAddress, int, int)}
+     * and {@link QosSocketFilter#matchesRemoteAddress(InetAddress, int, int)} with the
+     * filterSocketAddress coming from {@link QosSocketInfo#getLocalSocketAddress()}.
+     * <p>
+     * This method exists for testing purposes since {@link QosSocketInfo} couldn't be mocked
+     * due to being final.
+     *
+     * @param filterSocketAddress the socket address of the filter
+     * @param address the address to compare the filterSocketAddressWith
+     * @param startPort the start of the port range to check
+     * @param endPort the end of the port range to check
+     */
+    @VisibleForTesting
+    public static boolean matchesAddress(@NonNull final InetSocketAddress filterSocketAddress,
+            @NonNull final InetAddress address,
+            final int startPort, final int endPort) {
+        return startPort <= filterSocketAddress.getPort()
+                && endPort >= filterSocketAddress.getPort()
+                && filterSocketAddress.getAddress().equals(address);
+    }
+}
diff --git a/framework/src/android/net/QosSocketInfo.java b/framework/src/android/net/QosSocketInfo.java
new file mode 100644
index 0000000..a45d507
--- /dev/null
+++ b/framework/src/android/net/QosSocketInfo.java
@@ -0,0 +1,190 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/**
+ * Used in conjunction with
+ * {@link ConnectivityManager#registerQosCallback}
+ * in order to receive Qos Sessions related to the local address and port of a bound {@link Socket}
+ * and/or remote address and port of a connected {@link Socket}.
+ *
+ * @hide
+ */
+@SystemApi
+public final class QosSocketInfo implements Parcelable {
+
+    @NonNull
+    private final Network mNetwork;
+
+    @NonNull
+    private final ParcelFileDescriptor mParcelFileDescriptor;
+
+    @NonNull
+    private final InetSocketAddress mLocalSocketAddress;
+
+    @Nullable
+    private final InetSocketAddress mRemoteSocketAddress;
+
+    /**
+     * The {@link Network} the socket is on.
+     *
+     * @return the registered {@link Network}
+     */
+    @NonNull
+    public Network getNetwork() {
+        return mNetwork;
+    }
+
+    /**
+     * The parcel file descriptor wrapped around the socket's file descriptor.
+     *
+     * @return the parcel file descriptor of the socket
+     */
+    @NonNull
+    ParcelFileDescriptor getParcelFileDescriptor() {
+        return mParcelFileDescriptor;
+    }
+
+    /**
+     * The local address of the socket passed into {@link QosSocketInfo(Network, Socket)}.
+     * The value does not reflect any changes that occur to the socket after it is first set
+     * in the constructor.
+     *
+     * @return the local address of the socket
+     */
+    @NonNull
+    public InetSocketAddress getLocalSocketAddress() {
+        return mLocalSocketAddress;
+    }
+
+    /**
+     * The remote address of the socket passed into {@link QosSocketInfo(Network, Socket)}.
+     * The value does not reflect any changes that occur to the socket after it is first set
+     * in the constructor.
+     *
+     * @return the remote address of the socket if socket is connected, null otherwise
+     */
+    @Nullable
+    public InetSocketAddress getRemoteSocketAddress() {
+        return mRemoteSocketAddress;
+    }
+
+    /**
+     * 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.
+     *
+     * @param network the network
+     * @param socket the bound {@link Socket}
+     */
+    public QosSocketInfo(@NonNull final Network network, @NonNull final Socket socket)
+            throws IOException {
+        Objects.requireNonNull(socket, "socket cannot be null");
+
+        mNetwork = Objects.requireNonNull(network, "network cannot be null");
+        mParcelFileDescriptor = ParcelFileDescriptor.fromSocket(socket);
+        mLocalSocketAddress =
+                new InetSocketAddress(socket.getLocalAddress(), socket.getLocalPort());
+
+        if (socket.isConnected()) {
+            mRemoteSocketAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
+        } else {
+            mRemoteSocketAddress = null;
+        }
+    }
+
+    /* Parcelable methods */
+    private QosSocketInfo(final Parcel in) {
+        mNetwork = Objects.requireNonNull(Network.CREATOR.createFromParcel(in));
+        mParcelFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(in);
+
+        final int localAddressLength = in.readInt();
+        mLocalSocketAddress = readSocketAddress(in, localAddressLength);
+
+        final int remoteAddressLength = in.readInt();
+        mRemoteSocketAddress = remoteAddressLength == 0 ? null
+                : readSocketAddress(in, remoteAddressLength);
+    }
+
+    private @NonNull InetSocketAddress readSocketAddress(final Parcel in, final int addressLength) {
+        final byte[] address = new byte[addressLength];
+        in.readByteArray(address);
+        final int port = in.readInt();
+
+        try {
+            return new InetSocketAddress(InetAddress.getByAddress(address), port);
+        } catch (final UnknownHostException e) {
+            /* This can never happen. UnknownHostException will never be thrown
+               since the address provided is numeric and non-null. */
+            throw new RuntimeException("UnknownHostException on numeric address", e);
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull final Parcel dest, final int flags) {
+        mNetwork.writeToParcel(dest, 0);
+        mParcelFileDescriptor.writeToParcel(dest, 0);
+
+        final byte[] localAddress = mLocalSocketAddress.getAddress().getAddress();
+        dest.writeInt(localAddress.length);
+        dest.writeByteArray(localAddress);
+        dest.writeInt(mLocalSocketAddress.getPort());
+
+        if (mRemoteSocketAddress == null) {
+            dest.writeInt(0);
+        } else {
+            final byte[] remoteAddress = mRemoteSocketAddress.getAddress().getAddress();
+            dest.writeInt(remoteAddress.length);
+            dest.writeByteArray(remoteAddress);
+            dest.writeInt(mRemoteSocketAddress.getPort());
+        }
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<QosSocketInfo> CREATOR =
+            new Parcelable.Creator<QosSocketInfo>() {
+            @NonNull
+            @Override
+            public QosSocketInfo createFromParcel(final Parcel in) {
+                return new QosSocketInfo(in);
+            }
+
+            @NonNull
+            @Override
+            public QosSocketInfo[] newArray(final int size) {
+                return new QosSocketInfo[size];
+            }
+        };
+}
diff --git a/framework/src/android/net/RouteInfo.java b/framework/src/android/net/RouteInfo.java
new file mode 100644
index 0000000..fad3144
--- /dev/null
+++ b/framework/src/android/net/RouteInfo.java
@@ -0,0 +1,659 @@
+/*
+ * 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.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 com.android.net.module.util.NetUtils;
+import com.android.net.module.util.NetworkStackConstants;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * Represents a network route.
+ * <p>
+ * This is used both to describe static network configuration and live network
+ * configuration information.
+ *
+ * A route contains three pieces of information:
+ * <ul>
+ * <li>a destination {@link IpPrefix} specifying the network destinations covered by this route.
+ *     If this is {@code null} it indicates a default route of the address family (IPv4 or IPv6)
+ *     implied by the gateway IP address.
+ * <li>a gateway {@link InetAddress} indicating the next hop to use.  If this is {@code null} it
+ *     indicates a directly-connected route.
+ * <li>an interface (which may be unspecified).
+ * </ul>
+ * Either the destination or the gateway may be {@code null}, but not both.  If the
+ * destination and gateway are both specified, they must be of the same address family
+ * (IPv4 or IPv6).
+ */
+public final class RouteInfo implements Parcelable {
+    /** @hide */
+    @IntDef(value = {
+            RTN_UNICAST,
+            RTN_UNREACHABLE,
+            RTN_THROW,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RouteType {}
+
+    /**
+     * The IP destination address for this route.
+     */
+    @NonNull
+    private final IpPrefix mDestination;
+
+    /**
+     * The gateway address for this route.
+     */
+    @UnsupportedAppUsage
+    @Nullable
+    private final InetAddress mGateway;
+
+    /**
+     * The interface for this route.
+     */
+    @Nullable
+    private final String mInterface;
+
+
+    /** Unicast route. @hide */
+    @SystemApi
+    public static final int RTN_UNICAST = 1;
+
+    /** Unreachable route. @hide */
+    @SystemApi
+    public static final int RTN_UNREACHABLE = 7;
+
+    /** Throw route. @hide */
+    @SystemApi
+    public static final int RTN_THROW = 9;
+
+    /**
+     * The type of this route; one of the RTN_xxx constants above.
+     */
+    private final int mType;
+
+    /**
+     * The maximum transmission unit size for this route.
+     */
+    private final int mMtu;
+
+    // Derived data members.
+    // TODO: remove these.
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private final boolean mIsHost;
+    private final boolean mHasGateway;
+
+    /**
+     * Constructs a RouteInfo object.
+     *
+     * If destination is null, then gateway must be specified and the
+     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
+     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
+     * route <code>::/0</code> if gateway is an instance of
+     * {@link Inet6Address}.
+     * <p>
+     * destination and gateway may not both be null.
+     *
+     * @param destination the destination prefix
+     * @param gateway the IP address to route packets through
+     * @param iface the interface name to send packets on
+     * @param type the type of this route
+     *
+     * @hide
+     */
+    @SystemApi
+    public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway,
+            @Nullable String iface, @RouteType int type) {
+        this(destination, gateway, iface, type, 0);
+    }
+
+    /**
+     * Constructs a RouteInfo object.
+     *
+     * If destination is null, then gateway must be specified and the
+     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
+     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
+     * route <code>::/0</code> if gateway is an instance of
+     * {@link Inet6Address}.
+     * <p>
+     * destination and gateway may not both be null.
+     *
+     * @param destination the destination prefix
+     * @param gateway the IP address to route packets through
+     * @param iface the interface name to send packets on
+     * @param type the type of this route
+     * @param mtu the maximum transmission unit size for this route
+     *
+     * @hide
+     */
+    @SystemApi
+    public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway,
+            @Nullable String iface, @RouteType int type, int mtu) {
+        switch (type) {
+            case RTN_UNICAST:
+            case RTN_UNREACHABLE:
+            case RTN_THROW:
+                // TODO: It would be nice to ensure that route types that don't have nexthops or
+                // interfaces, such as unreachable or throw, can't be created if an interface or
+                // a gateway is specified. This is a bit too complicated to do at the moment
+                // because:
+                //
+                // - LinkProperties sets the interface on routes added to it, and modifies the
+                //   interfaces of all the routes when its interface name changes.
+                // - Even when the gateway is null, we store a non-null gateway here.
+                //
+                // For now, we just rely on the code that sets routes to do things properly.
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown route type " + type);
+        }
+
+        if (destination == null) {
+            if (gateway != null) {
+                if (gateway instanceof Inet4Address) {
+                    destination = new IpPrefix(NetworkStackConstants.IPV4_ADDR_ANY, 0);
+                } else {
+                    destination = new IpPrefix(NetworkStackConstants.IPV6_ADDR_ANY, 0);
+                }
+            } else {
+                // no destination, no gateway. invalid.
+                throw new IllegalArgumentException("Invalid arguments passed in: " + gateway + "," +
+                        destination);
+            }
+        }
+        // TODO: set mGateway to null if there is no gateway. This is more correct, saves space, and
+        // matches the documented behaviour. Before we can do this we need to fix all callers (e.g.,
+        // ConnectivityService) to stop doing things like r.getGateway().equals(), ... .
+        if (gateway == null) {
+            if (destination.getAddress() instanceof Inet4Address) {
+                gateway = NetworkStackConstants.IPV4_ADDR_ANY;
+            } else {
+                gateway = NetworkStackConstants.IPV6_ADDR_ANY;
+            }
+        }
+        mHasGateway = (!gateway.isAnyLocalAddress());
+
+        if ((destination.getAddress() instanceof Inet4Address
+                && !(gateway instanceof Inet4Address))
+                || (destination.getAddress() instanceof Inet6Address
+                && !(gateway instanceof Inet6Address))) {
+            throw new IllegalArgumentException("address family mismatch in RouteInfo constructor");
+        }
+        mDestination = destination;  // IpPrefix objects are immutable.
+        mGateway = gateway;          // InetAddress objects are immutable.
+        mInterface = iface;          // Strings are immutable.
+        mType = type;
+        mIsHost = isHost();
+        mMtu = mtu;
+    }
+
+    /**
+     * Constructs a {@code RouteInfo} object.
+     *
+     * If destination is null, then gateway must be specified and the
+     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
+     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
+     * route <code>::/0</code> if gateway is an instance of {@link Inet6Address}.
+     * <p>
+     * Destination and gateway may not both be null.
+     *
+     * @param destination the destination address and prefix in an {@link IpPrefix}
+     * @param gateway the {@link InetAddress} to route packets through
+     * @param iface the interface name to send packets on
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway,
+            @Nullable String iface) {
+        this(destination, gateway, iface, RTN_UNICAST);
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public RouteInfo(@Nullable LinkAddress destination, @Nullable InetAddress gateway,
+            @Nullable String iface) {
+        this(destination == null ? null :
+                new IpPrefix(destination.getAddress(), destination.getPrefixLength()),
+                gateway, iface);
+    }
+
+    /**
+     * Constructs a {@code RouteInfo} object.
+     *
+     * If destination is null, then gateway must be specified and the
+     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
+     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
+     * route <code>::/0</code> if gateway is an instance of {@link Inet6Address}.
+     * <p>
+     * Destination and gateway may not both be null.
+     *
+     * @param destination the destination address and prefix in an {@link IpPrefix}
+     * @param gateway the {@link InetAddress} to route packets through
+     *
+     * @hide
+     */
+    public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway) {
+        this(destination, gateway, null);
+    }
+
+    /**
+     * @hide
+     *
+     * TODO: Remove this.
+     */
+    @UnsupportedAppUsage
+    public RouteInfo(@Nullable LinkAddress destination, @Nullable InetAddress gateway) {
+        this(destination, gateway, null);
+    }
+
+    /**
+     * Constructs a default {@code RouteInfo} object.
+     *
+     * @param gateway the {@link InetAddress} to route packets through
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public RouteInfo(@NonNull InetAddress gateway) {
+        this((IpPrefix) null, gateway, null);
+    }
+
+    /**
+     * Constructs a {@code RouteInfo} object representing a direct connected subnet.
+     *
+     * @param destination the {@link IpPrefix} describing the address and prefix
+     *                    length of the subnet.
+     *
+     * @hide
+     */
+    public RouteInfo(@NonNull IpPrefix destination) {
+        this(destination, null, null);
+    }
+
+    /**
+     * @hide
+     */
+    public RouteInfo(@NonNull LinkAddress destination) {
+        this(destination, null, null);
+    }
+
+    /**
+     * @hide
+     */
+    public RouteInfo(@NonNull IpPrefix destination, @RouteType int type) {
+        this(destination, null, null, type);
+    }
+
+    /**
+     * @hide
+     */
+    public static RouteInfo makeHostRoute(@NonNull InetAddress host, @Nullable String iface) {
+        return makeHostRoute(host, null, iface);
+    }
+
+    /**
+     * @hide
+     */
+    public static RouteInfo makeHostRoute(@Nullable InetAddress host, @Nullable InetAddress gateway,
+            @Nullable String iface) {
+        if (host == null) return null;
+
+        if (host instanceof Inet4Address) {
+            return new RouteInfo(new IpPrefix(host, 32), gateway, iface);
+        } else {
+            return new RouteInfo(new IpPrefix(host, 128), gateway, iface);
+        }
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private boolean isHost() {
+        return (mDestination.getAddress() instanceof Inet4Address &&
+                mDestination.getPrefixLength() == 32) ||
+               (mDestination.getAddress() instanceof Inet6Address &&
+                mDestination.getPrefixLength() == 128);
+    }
+
+    /**
+     * Retrieves the destination address and prefix length in the form of an {@link IpPrefix}.
+     *
+     * @return {@link IpPrefix} specifying the destination.  This is never {@code null}.
+     */
+    @NonNull
+    public IpPrefix getDestination() {
+        return mDestination;
+    }
+
+    /**
+     * TODO: Convert callers to use IpPrefix and then remove.
+     * @hide
+     */
+    @NonNull
+    public LinkAddress getDestinationLinkAddress() {
+        return new LinkAddress(mDestination.getAddress(), mDestination.getPrefixLength());
+    }
+
+    /**
+     * Retrieves the gateway or next hop {@link InetAddress} for this route.
+     *
+     * @return {@link InetAddress} specifying the gateway or next hop.  This may be
+     *                             {@code null} for a directly-connected route."
+     */
+    @Nullable
+    public InetAddress getGateway() {
+        return mGateway;
+    }
+
+    /**
+     * Retrieves the interface used for this route if specified, else {@code null}.
+     *
+     * @return The name of the interface used for this route.
+     */
+    @Nullable
+    public String getInterface() {
+        return mInterface;
+    }
+
+    /**
+     * Retrieves the type of this route.
+     *
+     * @return The type of this route; one of the {@code RTN_xxx} constants defined in this class.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RouteType
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * Retrieves the MTU size for this route.
+     *
+     * @return The MTU size, or 0 if it has not been set.
+     * @hide
+     */
+    @SystemApi
+    public int getMtu() {
+        return mMtu;
+    }
+
+    /**
+     * Indicates if this route is a default route (ie, has no destination specified).
+     *
+     * @return {@code true} if the destination has a prefix length of 0.
+     */
+    public boolean isDefaultRoute() {
+        return mType == RTN_UNICAST && mDestination.getPrefixLength() == 0;
+    }
+
+    /**
+     * Indicates if this route is an unreachable default route.
+     *
+     * @return {@code true} if it's an unreachable route with prefix length of 0.
+     * @hide
+     */
+    private boolean isUnreachableDefaultRoute() {
+        return mType == RTN_UNREACHABLE && mDestination.getPrefixLength() == 0;
+    }
+
+    /**
+     * Indicates if this route is an IPv4 default route.
+     * @hide
+     */
+    public boolean isIPv4Default() {
+        return isDefaultRoute() && mDestination.getAddress() instanceof Inet4Address;
+    }
+
+    /**
+     * Indicates if this route is an IPv4 unreachable default route.
+     * @hide
+     */
+    public boolean isIPv4UnreachableDefault() {
+        return isUnreachableDefaultRoute() && mDestination.getAddress() instanceof Inet4Address;
+    }
+
+    /**
+     * Indicates if this route is an IPv6 default route.
+     * @hide
+     */
+    public boolean isIPv6Default() {
+        return isDefaultRoute() && mDestination.getAddress() instanceof Inet6Address;
+    }
+
+    /**
+     * Indicates if this route is an IPv6 unreachable default route.
+     * @hide
+     */
+    public boolean isIPv6UnreachableDefault() {
+        return isUnreachableDefaultRoute() && mDestination.getAddress() instanceof Inet6Address;
+    }
+
+    /**
+     * Indicates if this route is a host route (ie, matches only a single host address).
+     *
+     * @return {@code true} if the destination has a prefix length of 32 or 128 for IPv4 or IPv6,
+     * respectively.
+     * @hide
+     */
+    public boolean isHostRoute() {
+        return mIsHost;
+    }
+
+    /**
+     * Indicates if this route has a next hop ({@code true}) or is directly-connected
+     * ({@code false}).
+     *
+     * @return {@code true} if a gateway is specified
+     */
+    public boolean hasGateway() {
+        return mHasGateway;
+    }
+
+    /**
+     * Determines whether the destination and prefix of this route includes the specified
+     * address.
+     *
+     * @param destination A {@link InetAddress} to test to see if it would match this route.
+     * @return {@code true} if the destination and prefix length cover the given address.
+     */
+    public boolean matches(InetAddress destination) {
+        return mDestination.contains(destination);
+    }
+
+    /**
+     * Find the route from a Collection of routes that best matches a given address.
+     * May return null if no routes are applicable.
+     * @param routes a Collection of RouteInfos to chose from
+     * @param dest the InetAddress your trying to get to
+     * @return the RouteInfo from the Collection that best fits the given address
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    @Nullable
+    public static RouteInfo selectBestRoute(Collection<RouteInfo> routes, InetAddress dest) {
+        return NetUtils.selectBestRoute(routes, dest);
+    }
+
+    /**
+     * Returns a human-readable description of this object.
+     */
+    public String toString() {
+        String val = "";
+        if (mDestination != null) val = mDestination.toString();
+        if (mType == RTN_UNREACHABLE) {
+            val += " unreachable";
+        } else if (mType == RTN_THROW) {
+            val += " throw";
+        } else {
+            val += " ->";
+            if (mGateway != null) val += " " + mGateway.getHostAddress();
+            if (mInterface != null) val += " " + mInterface;
+            if (mType != RTN_UNICAST) {
+                val += " unknown type " + mType;
+            }
+        }
+        val += " mtu " + mMtu;
+        return val;
+    }
+
+    /**
+     * Compares this RouteInfo object against the specified object and indicates if they are equal.
+     * @return {@code true} if the objects are equal, {@code false} otherwise.
+     */
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof RouteInfo)) return false;
+
+        RouteInfo target = (RouteInfo) obj;
+
+        return Objects.equals(mDestination, target.getDestination()) &&
+                Objects.equals(mGateway, target.getGateway()) &&
+                Objects.equals(mInterface, target.getInterface()) &&
+                mType == target.getType() && mMtu == target.getMtu();
+    }
+
+    /**
+     * A helper class that contains the destination, the gateway and the interface in a
+     * {@code RouteInfo}, used by {@link ConnectivityService#updateRoutes} or
+     * {@link LinkProperties#addRoute} to calculate the list to be updated.
+     * {@code RouteInfo} objects with different interfaces are treated as different routes because
+     * *usually* on Android different interfaces use different routing tables, and moving a route
+     * to a new routing table never constitutes an update, but is always a remove and an add.
+     *
+     * @hide
+     */
+    public static class RouteKey {
+        @NonNull private final IpPrefix mDestination;
+        @Nullable private final InetAddress mGateway;
+        @Nullable private final String mInterface;
+
+        RouteKey(@NonNull IpPrefix destination, @Nullable InetAddress gateway,
+                @Nullable String iface) {
+            mDestination = destination;
+            mGateway = gateway;
+            mInterface = iface;
+        }
+
+        @Override
+        public boolean equals(@Nullable Object o) {
+            if (!(o instanceof RouteKey)) {
+                return false;
+            }
+            RouteKey p = (RouteKey) o;
+            // No need to do anything special for scoped addresses. Inet6Address#equals does not
+            // consider the scope ID, but the netd route IPCs (e.g., INetd#networkAddRouteParcel)
+            // and the kernel ignore scoped addresses both in the prefix and in the nexthop and only
+            // look at RTA_OIF.
+            return Objects.equals(p.mDestination, mDestination)
+                    && Objects.equals(p.mGateway, mGateway)
+                    && Objects.equals(p.mInterface, mInterface);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mDestination, mGateway, mInterface);
+        }
+    }
+
+    /**
+     * Get {@code RouteKey} of this {@code RouteInfo}.
+     * @return a {@code RouteKey} object.
+     *
+     * @hide
+     */
+    @NonNull
+    public RouteKey getRouteKey() {
+        return new RouteKey(mDestination, mGateway, mInterface);
+    }
+
+    /**
+     *  Returns a hashcode for this <code>RouteInfo</code> object.
+     */
+    public int hashCode() {
+        return (mDestination.hashCode() * 41)
+                + (mGateway == null ? 0 :mGateway.hashCode() * 47)
+                + (mInterface == null ? 0 :mInterface.hashCode() * 67)
+                + (mType * 71) + (mMtu * 89);
+    }
+
+    /**
+     * Implement the Parcelable interface
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implement the Parcelable interface
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mDestination, flags);
+        byte[] gatewayBytes = (mGateway == null) ? null : mGateway.getAddress();
+        dest.writeByteArray(gatewayBytes);
+        dest.writeString(mInterface);
+        dest.writeInt(mType);
+        dest.writeInt(mMtu);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public static final @android.annotation.NonNull Creator<RouteInfo> CREATOR =
+        new Creator<RouteInfo>() {
+        public RouteInfo createFromParcel(Parcel in) {
+            IpPrefix dest = in.readParcelable(null);
+
+            InetAddress gateway = null;
+            byte[] addr = in.createByteArray();
+            try {
+                gateway = InetAddress.getByAddress(addr);
+            } catch (UnknownHostException e) {}
+
+            String iface = in.readString();
+            int type = in.readInt();
+            int mtu = in.readInt();
+
+            return new RouteInfo(dest, gateway, iface, type, mtu);
+        }
+
+        public RouteInfo[] newArray(int size) {
+            return new RouteInfo[size];
+        }
+    };
+}
diff --git a/framework/src/android/net/SocketKeepalive.java b/framework/src/android/net/SocketKeepalive.java
new file mode 100644
index 0000000..f6cae72
--- /dev/null
+++ b/framework/src/android/net/SocketKeepalive.java
@@ -0,0 +1,347 @@
+/*
+ * 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;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows applications to request that the system periodically send specific packets on their
+ * behalf, using hardware offload to save battery power.
+ *
+ * To request that the system send keepalives, call one of the methods that return a
+ * {@link SocketKeepalive} object, such as {@link ConnectivityManager#createSocketKeepalive},
+ * passing in a non-null callback. If the {@link SocketKeepalive} is successfully
+ * started, the callback's {@code onStarted} method will be called. If an error occurs,
+ * {@code onError} will be called, specifying one of the {@code ERROR_*} constants in this
+ * class.
+ *
+ * To stop an existing keepalive, call {@link SocketKeepalive#stop}. The system will call
+ * {@link SocketKeepalive.Callback#onStopped} if the operation was successful or
+ * {@link SocketKeepalive.Callback#onError} if an error occurred.
+ *
+ * For cellular, the device MUST support at least 1 keepalive slot.
+ *
+ * For WiFi, the device SHOULD support keepalive offload. If it does not, it MUST reply with
+ * {@link SocketKeepalive.Callback#onError} with {@code ERROR_UNSUPPORTED} to any keepalive offload
+ * request. If it does, it MUST support at least 3 concurrent keepalive slots.
+ */
+public abstract class SocketKeepalive implements AutoCloseable {
+    static final String TAG = "SocketKeepalive";
+
+    /**
+     * Success. It indicates there is no error.
+     * @hide
+     */
+    @SystemApi
+    public static final int SUCCESS = 0;
+
+    /**
+     * No keepalive. This should only be internally as it indicates There is no keepalive.
+     * It should not propagate to applications.
+     * @hide
+     */
+    public static final int NO_KEEPALIVE = -1;
+
+    /**
+     * Data received.
+     * @hide
+     */
+    public static final int DATA_RECEIVED = -2;
+
+    /**
+     * The binder died.
+     * @hide
+     */
+    public static final int BINDER_DIED = -10;
+
+    /**
+     * The invalid network. It indicates the specified {@code Network} is not connected.
+     */
+    public static final int ERROR_INVALID_NETWORK = -20;
+
+    /**
+     * The invalid IP addresses. Indicates the specified IP addresses are invalid.
+     * For example, the specified source IP address is not configured on the
+     * specified {@code Network}.
+     */
+    public static final int ERROR_INVALID_IP_ADDRESS = -21;
+
+    /**
+     * The port is invalid.
+     */
+    public static final int ERROR_INVALID_PORT = -22;
+
+    /**
+     * The length is invalid (e.g. too long).
+     */
+    public static final int ERROR_INVALID_LENGTH = -23;
+
+    /**
+     * The interval is invalid (e.g. too short).
+     */
+    public static final int ERROR_INVALID_INTERVAL = -24;
+
+    /**
+     * The socket is invalid.
+     */
+    public static final int ERROR_INVALID_SOCKET = -25;
+
+    /**
+     * The socket is not idle.
+     */
+    public static final int ERROR_SOCKET_NOT_IDLE = -26;
+
+    /**
+     * The stop reason is uninitialized. This should only be internally used as initial state
+     * of stop reason, instead of propagating to application.
+     * @hide
+     */
+    public static final int ERROR_STOP_REASON_UNINITIALIZED = -27;
+
+    /**
+     * The request is unsupported.
+     */
+    public static final int ERROR_UNSUPPORTED = -30;
+
+    /**
+     * There was a hardware error.
+     */
+    public static final int ERROR_HARDWARE_ERROR = -31;
+
+    /**
+     * Resources are insufficient (e.g. all hardware slots are in use).
+     */
+    public static final int ERROR_INSUFFICIENT_RESOURCES = -32;
+
+    /**
+     * There was no such slot. This should only be internally as it indicates
+     * a programming error in the system server. It should not propagate to
+     * applications.
+     * @hide
+     */
+    @SystemApi
+    public static final int ERROR_NO_SUCH_SLOT = -33;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "ERROR_" }, value = {
+            ERROR_INVALID_NETWORK,
+            ERROR_INVALID_IP_ADDRESS,
+            ERROR_INVALID_PORT,
+            ERROR_INVALID_LENGTH,
+            ERROR_INVALID_INTERVAL,
+            ERROR_INVALID_SOCKET,
+            ERROR_SOCKET_NOT_IDLE,
+            ERROR_NO_SUCH_SLOT
+    })
+    public @interface ErrorCode {}
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            SUCCESS,
+            ERROR_INVALID_LENGTH,
+            ERROR_UNSUPPORTED,
+            ERROR_INSUFFICIENT_RESOURCES,
+    })
+    public @interface KeepaliveEvent {}
+
+    /**
+     * The minimum interval in seconds between keepalive packet transmissions.
+     *
+     * @hide
+     **/
+    public static final int MIN_INTERVAL_SEC = 10;
+
+    /**
+     * The maximum interval in seconds between keepalive packet transmissions.
+     *
+     * @hide
+     **/
+    public static final int MAX_INTERVAL_SEC = 3600;
+
+    /**
+     * An exception that embarks an error code.
+     * @hide
+     */
+    public static class ErrorCodeException extends Exception {
+        public final int error;
+        public ErrorCodeException(final int error, final Throwable e) {
+            super(e);
+            this.error = error;
+        }
+        public ErrorCodeException(final int error) {
+            this.error = error;
+        }
+    }
+
+    /**
+     * This socket is invalid.
+     * See the error code for details, and the optional cause.
+     * @hide
+     */
+    public static class InvalidSocketException extends ErrorCodeException {
+        public InvalidSocketException(final int error, final Throwable e) {
+            super(error, e);
+        }
+        public InvalidSocketException(final int error) {
+            super(error);
+        }
+    }
+
+    @NonNull final IConnectivityManager mService;
+    @NonNull final Network mNetwork;
+    @NonNull final ParcelFileDescriptor mPfd;
+    @NonNull final Executor mExecutor;
+    @NonNull final ISocketKeepaliveCallback mCallback;
+    // TODO: remove slot since mCallback could be used to identify which keepalive to stop.
+    @Nullable Integer mSlot;
+
+    SocketKeepalive(@NonNull IConnectivityManager service, @NonNull Network network,
+            @NonNull ParcelFileDescriptor pfd,
+            @NonNull Executor executor, @NonNull Callback callback) {
+        mService = service;
+        mNetwork = network;
+        mPfd = pfd;
+        mExecutor = executor;
+        mCallback = new ISocketKeepaliveCallback.Stub() {
+            @Override
+            public void onStarted(int slot) {
+                final long token = Binder.clearCallingIdentity();
+                try {
+                    mExecutor.execute(() -> {
+                        mSlot = slot;
+                        callback.onStarted();
+                    });
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
+            }
+
+            @Override
+            public void onStopped() {
+                final long token = Binder.clearCallingIdentity();
+                try {
+                    executor.execute(() -> {
+                        mSlot = null;
+                        callback.onStopped();
+                    });
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
+            }
+
+            @Override
+            public void onError(int error) {
+                final long token = Binder.clearCallingIdentity();
+                try {
+                    executor.execute(() -> {
+                        mSlot = null;
+                        callback.onError(error);
+                    });
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
+            }
+
+            @Override
+            public void onDataReceived() {
+                final long token = Binder.clearCallingIdentity();
+                try {
+                    executor.execute(() -> {
+                        mSlot = null;
+                        callback.onDataReceived();
+                    });
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
+            }
+        };
+    }
+
+    /**
+     * Request that keepalive be started with the given {@code intervalSec}. See
+     * {@link SocketKeepalive}. If the remote binder dies, or the binder call throws an exception
+     * when invoking start or stop of the {@link SocketKeepalive}, a {@link RemoteException} will be
+     * thrown into the {@code executor}. This is typically not important to catch because the remote
+     * party is the system, so if it is not in shape to communicate through binder the system is
+     * probably going down anyway. If the caller cares regardless, it can use a custom
+     * {@link Executor} to catch the {@link RemoteException}.
+     *
+     * @param intervalSec The target interval in seconds between keepalive packet transmissions.
+     *                    The interval should be between 10 seconds and 3600 seconds, otherwise
+     *                    {@link #ERROR_INVALID_INTERVAL} will be returned.
+     */
+    public final void start(@IntRange(from = MIN_INTERVAL_SEC, to = MAX_INTERVAL_SEC)
+            int intervalSec) {
+        startImpl(intervalSec);
+    }
+
+    abstract void startImpl(int intervalSec);
+
+    /**
+     * Requests that keepalive be stopped. The application must wait for {@link Callback#onStopped}
+     * before using the object. See {@link SocketKeepalive}.
+     */
+    public final void stop() {
+        stopImpl();
+    }
+
+    abstract void stopImpl();
+
+    /**
+     * Deactivate this {@link SocketKeepalive} and free allocated resources. The instance won't be
+     * usable again if {@code close()} is called.
+     */
+    @Override
+    public final void close() {
+        stop();
+        try {
+            mPfd.close();
+        } catch (IOException e) {
+            // Nothing much can be done.
+        }
+    }
+
+    /**
+     * The callback which app can use to learn the status changes of {@link SocketKeepalive}. See
+     * {@link SocketKeepalive}.
+     */
+    public static class Callback {
+        /** The requested keepalive was successfully started. */
+        public void onStarted() {}
+        /** The keepalive was successfully stopped. */
+        public void onStopped() {}
+        /** An error occurred. */
+        public void onError(@ErrorCode int error) {}
+        /** The keepalive on a TCP socket was stopped because the socket received data. This is
+         * never called for UDP sockets. */
+        public void onDataReceived() {}
+    }
+}
diff --git a/framework/src/android/net/SocketLocalAddressChangedException.java b/framework/src/android/net/SocketLocalAddressChangedException.java
new file mode 100644
index 0000000..9daad83
--- /dev/null
+++ b/framework/src/android/net/SocketLocalAddressChangedException.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * Thrown when the local address of the socket has changed.
+ *
+ * @hide
+ */
+@SystemApi
+public class SocketLocalAddressChangedException extends Exception {
+    /** @hide */
+    public SocketLocalAddressChangedException() {
+        super("The local address of the socket changed");
+    }
+}
diff --git a/framework/src/android/net/SocketNotBoundException.java b/framework/src/android/net/SocketNotBoundException.java
new file mode 100644
index 0000000..b1d7026
--- /dev/null
+++ b/framework/src/android/net/SocketNotBoundException.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * Thrown when a previously bound socket becomes unbound.
+ *
+ * @hide
+ */
+@SystemApi
+public class SocketNotBoundException extends Exception {
+    /** @hide */
+    public SocketNotBoundException() {
+        super("The socket is unbound");
+    }
+}
diff --git a/framework/src/android/net/StaticIpConfiguration.java b/framework/src/android/net/StaticIpConfiguration.java
new file mode 100644
index 0000000..7904f7a
--- /dev/null
+++ b/framework/src/android/net/StaticIpConfiguration.java
@@ -0,0 +1,331 @@
+/*
+ * 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.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 com.android.net.module.util.InetAddressUtils;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Class that describes static IP configuration.
+ *
+ * <p>This class is different from {@link LinkProperties} because it represents
+ * configuration intent. The general contract is that if we can represent
+ * a configuration here, then we should be able to configure it on a network.
+ * The intent is that it closely match the UI we have for configuring networks.
+ *
+ * <p>In contrast, {@link LinkProperties} represents current state. It is much more
+ * expressive. For example, it supports multiple IP addresses, multiple routes,
+ * stacked interfaces, and so on. Because LinkProperties is so expressive,
+ * using it to represent configuration intent as well as current state causes
+ * problems. For example, we could unknowingly save a configuration that we are
+ * not in fact capable of applying, or we could save a configuration that the
+ * UI cannot display, which has the potential for malicious code to hide
+ * hostile or unexpected configuration from the user.
+ *
+ * @hide
+ */
+@SystemApi
+public final class StaticIpConfiguration implements Parcelable {
+    /** @hide */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    @Nullable
+    public LinkAddress ipAddress;
+    /** @hide */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    @Nullable
+    public InetAddress gateway;
+    /** @hide */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    @NonNull
+    public final ArrayList<InetAddress> dnsServers;
+    /** @hide */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    @Nullable
+    public String domains;
+
+    public StaticIpConfiguration() {
+        dnsServers = new ArrayList<>();
+    }
+
+    public StaticIpConfiguration(@Nullable StaticIpConfiguration source) {
+        this();
+        if (source != null) {
+            // All of these except dnsServers are immutable, so no need to make copies.
+            ipAddress = source.ipAddress;
+            gateway = source.gateway;
+            dnsServers.addAll(source.dnsServers);
+            domains = source.domains;
+        }
+    }
+
+    public void clear() {
+        ipAddress = null;
+        gateway = null;
+        dnsServers.clear();
+        domains = null;
+    }
+
+    /**
+     * Get the static IP address included in the configuration.
+     */
+    public @Nullable LinkAddress getIpAddress() {
+        return ipAddress;
+    }
+
+    /**
+     * Get the gateway included in the configuration.
+     */
+    public @Nullable InetAddress getGateway() {
+        return gateway;
+    }
+
+    /**
+     * Get the DNS servers included in the configuration.
+     */
+    public @NonNull List<InetAddress> getDnsServers() {
+        return dnsServers;
+    }
+
+    /**
+     * Get a {@link String} containing the comma separated domains to search when resolving host
+     * names on this link, in priority order.
+     */
+    public @Nullable String getDomains() {
+        return domains;
+    }
+
+    /**
+     * Helper class to build a new instance of {@link StaticIpConfiguration}.
+     */
+    public static final class Builder {
+        private LinkAddress mIpAddress;
+        private InetAddress mGateway;
+        private Iterable<InetAddress> mDnsServers;
+        private String mDomains;
+
+        /**
+         * Set the IP address to be included in the configuration; null by default.
+         * @return The {@link Builder} for chaining.
+         */
+        public @NonNull Builder setIpAddress(@Nullable LinkAddress ipAddress) {
+            mIpAddress = ipAddress;
+            return this;
+        }
+
+        /**
+         * Set the address of the gateway to be included in the configuration; null by default.
+         * @return The {@link Builder} for chaining.
+         */
+        public @NonNull Builder setGateway(@Nullable InetAddress gateway) {
+            mGateway = gateway;
+            return this;
+        }
+
+        /**
+         * Set the addresses of the DNS servers included in the configuration; empty by default.
+         * @return The {@link Builder} for chaining.
+         */
+        public @NonNull Builder setDnsServers(@NonNull Iterable<InetAddress> dnsServers) {
+            Objects.requireNonNull(dnsServers);
+            mDnsServers = dnsServers;
+            return this;
+        }
+
+        /**
+         * Sets the DNS domain search path to be used on the link; null by default.
+         * @param newDomains A {@link String} containing the comma separated domains to search when
+         *                   resolving host names on this link, in priority order.
+         * @return The {@link Builder} for chaining.
+         */
+        public @NonNull Builder setDomains(@Nullable String newDomains) {
+            mDomains = newDomains;
+            return this;
+        }
+
+        /**
+         * Create a {@link StaticIpConfiguration} from the parameters in this {@link Builder}.
+         * @return The newly created StaticIpConfiguration.
+         */
+        public @NonNull StaticIpConfiguration build() {
+            final StaticIpConfiguration config = new StaticIpConfiguration();
+            config.ipAddress = mIpAddress;
+            config.gateway = mGateway;
+            if (mDnsServers != null) {
+                for (InetAddress server : mDnsServers) {
+                    config.dnsServers.add(server);
+                }
+            }
+            config.domains = mDomains;
+            return config;
+        }
+    }
+
+    /**
+     * Add a DNS server to this configuration.
+     */
+    public void addDnsServer(@NonNull InetAddress server) {
+        dnsServers.add(server);
+    }
+
+    /**
+     * Returns the network routes specified by this object. Will typically include a
+     * directly-connected route for the IP address's local subnet and a default route.
+     * @param iface Interface to include in the routes.
+     */
+    public @NonNull List<RouteInfo> getRoutes(@Nullable String iface) {
+        List<RouteInfo> routes = new ArrayList<RouteInfo>(3);
+        if (ipAddress != null) {
+            RouteInfo connectedRoute = new RouteInfo(ipAddress, null, iface);
+            routes.add(connectedRoute);
+            // If the default gateway is not covered by the directly-connected route, also add a
+            // host route to the gateway as well. This configuration is arguably invalid, but it
+            // used to work in K and earlier, and other OSes appear to accept it.
+            if (gateway != null && !connectedRoute.matches(gateway)) {
+                routes.add(RouteInfo.makeHostRoute(gateway, iface));
+            }
+        }
+        if (gateway != null) {
+            routes.add(new RouteInfo((IpPrefix) null, gateway, iface));
+        }
+        return routes;
+    }
+
+    /**
+     * Returns a LinkProperties object expressing the data in this object. Note that the information
+     * contained in the LinkProperties will not be a complete picture of the link's configuration,
+     * because any configuration information that is obtained dynamically by the network (e.g.,
+     * IPv6 configuration) will not be included.
+     * @hide
+     */
+    public @NonNull LinkProperties toLinkProperties(String iface) {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(iface);
+        if (ipAddress != null) {
+            lp.addLinkAddress(ipAddress);
+        }
+        for (RouteInfo route : getRoutes(iface)) {
+            lp.addRoute(route);
+        }
+        for (InetAddress dns : dnsServers) {
+            lp.addDnsServer(dns);
+        }
+        lp.setDomains(domains);
+        return lp;
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        StringBuffer str = new StringBuffer();
+
+        str.append("IP address ");
+        if (ipAddress != null ) str.append(ipAddress).append(" ");
+
+        str.append("Gateway ");
+        if (gateway != null) str.append(gateway.getHostAddress()).append(" ");
+
+        str.append(" DNS servers: [");
+        for (InetAddress dnsServer : dnsServers) {
+            str.append(" ").append(dnsServer.getHostAddress());
+        }
+
+        str.append(" ] Domains ");
+        if (domains != null) str.append(domains);
+        return str.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 13;
+        result = 47 * result + (ipAddress == null ? 0 : ipAddress.hashCode());
+        result = 47 * result + (gateway == null ? 0 : gateway.hashCode());
+        result = 47 * result + (domains == null ? 0 : domains.hashCode());
+        result = 47 * result + dnsServers.hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof StaticIpConfiguration)) return false;
+
+        StaticIpConfiguration other = (StaticIpConfiguration) obj;
+
+        return other != null &&
+                Objects.equals(ipAddress, other.ipAddress) &&
+                Objects.equals(gateway, other.gateway) &&
+                dnsServers.equals(other.dnsServers) &&
+                Objects.equals(domains, other.domains);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<StaticIpConfiguration> CREATOR =
+        new Creator<StaticIpConfiguration>() {
+            public StaticIpConfiguration createFromParcel(Parcel in) {
+                return readFromParcel(in);
+            }
+
+            public StaticIpConfiguration[] newArray(int size) {
+                return new StaticIpConfiguration[size];
+            }
+        };
+
+    /** Implement the Parcelable interface */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(ipAddress, flags);
+        InetAddressUtils.parcelInetAddress(dest, gateway, flags);
+        dest.writeInt(dnsServers.size());
+        for (InetAddress dnsServer : dnsServers) {
+            InetAddressUtils.parcelInetAddress(dest, dnsServer, flags);
+        }
+        dest.writeString(domains);
+    }
+
+    /** @hide */
+    public static StaticIpConfiguration readFromParcel(Parcel in) {
+        final StaticIpConfiguration s = new StaticIpConfiguration();
+        s.ipAddress = in.readParcelable(null);
+        s.gateway = InetAddressUtils.unparcelInetAddress(in);
+        s.dnsServers.clear();
+        int size = in.readInt();
+        for (int i = 0; i < size; i++) {
+            s.dnsServers.add(InetAddressUtils.unparcelInetAddress(in));
+        }
+        s.domains = in.readString();
+        return s;
+    }
+}
diff --git a/framework/src/android/net/TcpKeepalivePacketData.java b/framework/src/android/net/TcpKeepalivePacketData.java
new file mode 100644
index 0000000..c2c4f32
--- /dev/null
+++ b/framework/src/android/net/TcpKeepalivePacketData.java
@@ -0,0 +1,230 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.InetAddress;
+import java.util.Objects;
+
+/**
+ * Represents the actual tcp keep alive packets which will be used for hardware offload.
+ * @hide
+ */
+@SystemApi
+public final class TcpKeepalivePacketData extends KeepalivePacketData implements Parcelable {
+    private static final String TAG = "TcpKeepalivePacketData";
+
+    /**
+     * TCP sequence number.
+     * @hide
+     */
+    public final int tcpSeq;
+
+    /**
+     * TCP ACK number.
+     * @hide
+     */
+    public final int tcpAck;
+
+    /**
+     * TCP RCV window.
+     * @hide
+     */
+    public final int tcpWindow;
+
+    /** TCP RCV window scale.
+     * @hide
+     */
+    public final int tcpWindowScale;
+
+    /**
+     * IP TOS.
+     * @hide
+     */
+    public final int ipTos;
+
+    /**
+     * IP TTL.
+     * @hide
+     */
+    public final int ipTtl;
+
+    public TcpKeepalivePacketData(@NonNull final InetAddress srcAddress, int srcPort,
+            @NonNull final InetAddress dstAddress, int dstPort, @NonNull final byte[] data,
+            int tcpSeq, int tcpAck, int tcpWindow, int tcpWindowScale, int ipTos, int ipTtl)
+            throws InvalidPacketException {
+        super(srcAddress, srcPort, dstAddress, dstPort, data);
+        this.tcpSeq = tcpSeq;
+        this.tcpAck = tcpAck;
+        this.tcpWindow = tcpWindow;
+        this.tcpWindowScale = tcpWindowScale;
+        this.ipTos = ipTos;
+        this.ipTtl = ipTtl;
+    }
+
+    /**
+     * Get the TCP sequence number.
+     *
+     * See https://tools.ietf.org/html/rfc793#page-15.
+     */
+    public int getTcpSeq() {
+        return tcpSeq;
+    }
+
+    /**
+     * Get the TCP ACK number.
+     *
+     * See https://tools.ietf.org/html/rfc793#page-15.
+     */
+    public int getTcpAck() {
+        return tcpAck;
+    }
+
+    /**
+     * Get the TCP RCV window.
+     *
+     * See https://tools.ietf.org/html/rfc793#page-15.
+     */
+    public int getTcpWindow() {
+        return tcpWindow;
+    }
+
+    /**
+     * Get the TCP RCV window scale.
+     *
+     * See https://tools.ietf.org/html/rfc793#page-15.
+     */
+    public int getTcpWindowScale() {
+        return tcpWindowScale;
+    }
+
+    /**
+     * Get the IP type of service.
+     */
+    public int getIpTos() {
+        return ipTos;
+    }
+
+    /**
+     * Get the IP TTL.
+     */
+    public int getIpTtl() {
+        return ipTtl;
+    }
+
+    @Override
+    public boolean equals(@Nullable final Object o) {
+        if (!(o instanceof TcpKeepalivePacketData)) return false;
+        final TcpKeepalivePacketData other = (TcpKeepalivePacketData) o;
+        final InetAddress srcAddress = getSrcAddress();
+        final InetAddress dstAddress = getDstAddress();
+        return srcAddress.equals(other.getSrcAddress())
+                && dstAddress.equals(other.getDstAddress())
+                && getSrcPort() == other.getSrcPort()
+                && getDstPort() == other.getDstPort()
+                && this.tcpAck == other.tcpAck
+                && this.tcpSeq == other.tcpSeq
+                && this.tcpWindow == other.tcpWindow
+                && this.tcpWindowScale == other.tcpWindowScale
+                && this.ipTos == other.ipTos
+                && this.ipTtl == other.ipTtl;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getSrcAddress(), getDstAddress(), getSrcPort(), getDstPort(),
+                tcpAck, tcpSeq, tcpWindow, tcpWindowScale, ipTos, ipTtl);
+    }
+
+    /**
+     * Parcelable Implementation.
+     * Note that this object implements parcelable (and needs to keep doing this as it inherits
+     * from a class that does), but should usually be parceled as a stable parcelable using
+     * the toStableParcelable() and fromStableParcelable() methods.
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Write to parcel. */
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeString(getSrcAddress().getHostAddress());
+        out.writeString(getDstAddress().getHostAddress());
+        out.writeInt(getSrcPort());
+        out.writeInt(getDstPort());
+        out.writeByteArray(getPacket());
+        out.writeInt(tcpSeq);
+        out.writeInt(tcpAck);
+        out.writeInt(tcpWindow);
+        out.writeInt(tcpWindowScale);
+        out.writeInt(ipTos);
+        out.writeInt(ipTtl);
+    }
+
+    private static TcpKeepalivePacketData readFromParcel(Parcel in) throws InvalidPacketException {
+        InetAddress srcAddress = InetAddresses.parseNumericAddress(in.readString());
+        InetAddress dstAddress = InetAddresses.parseNumericAddress(in.readString());
+        int srcPort = in.readInt();
+        int dstPort = in.readInt();
+        byte[] packet = in.createByteArray();
+        int tcpSeq = in.readInt();
+        int tcpAck = in.readInt();
+        int tcpWnd = in.readInt();
+        int tcpWndScale = in.readInt();
+        int ipTos = in.readInt();
+        int ipTtl = in.readInt();
+        return new TcpKeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, packet, tcpSeq,
+                tcpAck, tcpWnd, tcpWndScale, ipTos, ipTtl);
+    }
+
+    /** Parcelable Creator. */
+    public static final @NonNull Parcelable.Creator<TcpKeepalivePacketData> CREATOR =
+            new Parcelable.Creator<TcpKeepalivePacketData>() {
+                public TcpKeepalivePacketData createFromParcel(Parcel in) {
+                    try {
+                        return readFromParcel(in);
+                    } catch (InvalidPacketException e) {
+                        throw new IllegalArgumentException(
+                                "Invalid TCP keepalive data: " + e.getError());
+                    }
+                }
+
+                public TcpKeepalivePacketData[] newArray(int size) {
+                    return new TcpKeepalivePacketData[size];
+                }
+            };
+
+    @Override
+    public String toString() {
+        return "saddr: " + getSrcAddress()
+                + " daddr: " + getDstAddress()
+                + " sport: " + getSrcPort()
+                + " dport: " + getDstPort()
+                + " seq: " + tcpSeq
+                + " ack: " + tcpAck
+                + " window: " + tcpWindow
+                + " windowScale: " + tcpWindowScale
+                + " tos: " + ipTos
+                + " ttl: " + ipTtl;
+    }
+}
diff --git a/framework/src/android/net/TcpRepairWindow.java b/framework/src/android/net/TcpRepairWindow.java
new file mode 100644
index 0000000..86034f0
--- /dev/null
+++ b/framework/src/android/net/TcpRepairWindow.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+/**
+ * Corresponds to C's {@code struct tcp_repair_window} from
+ * include/uapi/linux/tcp.h
+ *
+ * @hide
+ */
+public final class TcpRepairWindow {
+    public final int sndWl1;
+    public final int sndWnd;
+    public final int maxWindow;
+    public final int rcvWnd;
+    public final int rcvWup;
+    public final int rcvWndScale;
+
+    /**
+     * Constructs an instance with the given field values.
+     */
+    public TcpRepairWindow(final int sndWl1, final int sndWnd, final int maxWindow,
+            final int rcvWnd, final int rcvWup, final int rcvWndScale) {
+        this.sndWl1 = sndWl1;
+        this.sndWnd = sndWnd;
+        this.maxWindow = maxWindow;
+        this.rcvWnd = rcvWnd;
+        this.rcvWup = rcvWup;
+        this.rcvWndScale = rcvWndScale;
+    }
+}
diff --git a/framework/src/android/net/TcpSocketKeepalive.java b/framework/src/android/net/TcpSocketKeepalive.java
new file mode 100644
index 0000000..d89814d
--- /dev/null
+++ b/framework/src/android/net/TcpSocketKeepalive.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.concurrent.Executor;
+
+/** @hide */
+final class TcpSocketKeepalive extends SocketKeepalive {
+
+    TcpSocketKeepalive(@NonNull IConnectivityManager service,
+            @NonNull Network network,
+            @NonNull ParcelFileDescriptor pfd,
+            @NonNull Executor executor,
+            @NonNull Callback callback) {
+        super(service, network, pfd, executor, callback);
+    }
+
+    /**
+     * Starts keepalives. {@code mSocket} must be a connected TCP socket.
+     *
+     * - The application must not write to or read from the socket after calling this method, until
+     *   onDataReceived, onStopped, or onError are called. If it does, the keepalive will fail
+     *   with {@link #ERROR_SOCKET_NOT_IDLE}, or {@code #ERROR_INVALID_SOCKET} if the socket
+     *   experienced an error (as in poll(2) returned POLLERR or POLLHUP); if this happens, the data
+     *   received from the socket may be invalid, and the socket can't be recovered.
+     * - If the socket has data in the send or receive buffer, then this call will fail with
+     *   {@link #ERROR_SOCKET_NOT_IDLE} and can be retried after the data has been processed.
+     *   An app could ensure this by using an application-layer protocol to receive acknowledgement
+     *   that indicates all data has been delivered to server, e.g. HTTP 200 OK.
+     *   Then the app could go into keepalive mode after reading all remaining data within the
+     *   acknowledgement.
+     */
+    @Override
+    void startImpl(int intervalSec) {
+        mExecutor.execute(() -> {
+            try {
+                mService.startTcpKeepalive(mNetwork, mPfd, intervalSec, mCallback);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error starting packet keepalive: ", e);
+                throw e.rethrowFromSystemServer();
+            }
+        });
+    }
+
+    @Override
+    void stopImpl() {
+        mExecutor.execute(() -> {
+            try {
+                if (mSlot != null) {
+                    mService.stopKeepalive(mNetwork, mSlot);
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error stopping packet keepalive: ", e);
+                throw e.rethrowFromSystemServer();
+            }
+        });
+    }
+}
diff --git a/framework/src/android/net/TestNetworkInterface.java b/framework/src/android/net/TestNetworkInterface.java
new file mode 100644
index 0000000..4449ff8
--- /dev/null
+++ b/framework/src/android/net/TestNetworkInterface.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+
+/**
+ * This class is used to return the interface name and fd of the test interface
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class TestNetworkInterface implements Parcelable {
+    @NonNull
+    private final ParcelFileDescriptor mFileDescriptor;
+    @NonNull
+    private final String mInterfaceName;
+
+    @Override
+    public int describeContents() {
+        return (mFileDescriptor != null) ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeParcelable(mFileDescriptor, PARCELABLE_WRITE_RETURN_VALUE);
+        out.writeString(mInterfaceName);
+    }
+
+    public TestNetworkInterface(@NonNull ParcelFileDescriptor pfd, @NonNull String intf) {
+        mFileDescriptor = pfd;
+        mInterfaceName = intf;
+    }
+
+    private TestNetworkInterface(@NonNull Parcel in) {
+        mFileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
+        mInterfaceName = in.readString();
+    }
+
+    @NonNull
+    public ParcelFileDescriptor getFileDescriptor() {
+        return mFileDescriptor;
+    }
+
+    @NonNull
+    public String getInterfaceName() {
+        return mInterfaceName;
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<TestNetworkInterface> CREATOR =
+            new Parcelable.Creator<TestNetworkInterface>() {
+                public TestNetworkInterface createFromParcel(Parcel in) {
+                    return new TestNetworkInterface(in);
+                }
+
+                public TestNetworkInterface[] newArray(int size) {
+                    return new TestNetworkInterface[size];
+                }
+            };
+}
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
new file mode 100644
index 0000000..9ddd2f5
--- /dev/null
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -0,0 +1,182 @@
+/*
+ * 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.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * Class that allows creation and management of per-app, test-only networks
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public class TestNetworkManager {
+    /**
+     * Prefix for tun interfaces created by this class.
+     * @hide
+     */
+    public static final String TEST_TUN_PREFIX = "testtun";
+
+    /**
+     * Prefix for tap interfaces created by this class.
+     */
+    public static final String TEST_TAP_PREFIX = "testtap";
+
+    @NonNull private static final String TAG = TestNetworkManager.class.getSimpleName();
+
+    @NonNull private final ITestNetworkManager mService;
+
+    /** @hide */
+    public TestNetworkManager(@NonNull ITestNetworkManager service) {
+        mService = Objects.requireNonNull(service, "missing ITestNetworkManager");
+    }
+
+    /**
+     * Teardown the capability-limited, testing-only network for a given interface
+     *
+     * @param network The test network that should be torn down
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public void teardownTestNetwork(@NonNull Network network) {
+        try {
+            mService.teardownTestNetwork(network.netId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private void setupTestNetwork(
+            @NonNull String iface,
+            @Nullable LinkProperties lp,
+            boolean isMetered,
+            @NonNull int[] administratorUids,
+            @NonNull IBinder binder) {
+        try {
+            mService.setupTestNetwork(iface, lp, isMetered, administratorUids, binder);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Sets up a capability-limited, testing-only network for a given interface
+     *
+     * @param lp The LinkProperties for the TestNetworkService to use for this test network. Note
+     *     that the interface name and link addresses will be overwritten, and the passed-in values
+     *     discarded.
+     * @param isMetered Whether or not the network should be considered metered.
+     * @param binder A binder object guarding the lifecycle of this test network.
+     * @hide
+     */
+    public void setupTestNetwork(
+            @NonNull LinkProperties lp, boolean isMetered, @NonNull IBinder binder) {
+        Objects.requireNonNull(lp, "Invalid LinkProperties");
+        setupTestNetwork(lp.getInterfaceName(), lp, isMetered, new int[0], binder);
+    }
+
+    /**
+     * Sets up a capability-limited, testing-only network for a given interface
+     *
+     * @param iface the name of the interface to be used for the Network LinkProperties.
+     * @param binder A binder object guarding the lifecycle of this test network.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public void setupTestNetwork(@NonNull String iface, @NonNull IBinder binder) {
+        setupTestNetwork(iface, null, true, new int[0], binder);
+    }
+
+    /**
+     * Sets up a capability-limited, testing-only network for a given interface with the given
+     * administrator UIDs.
+     *
+     * @param iface the name of the interface to be used for the Network LinkProperties.
+     * @param administratorUids The administrator UIDs to be used for the test-only network
+     * @param binder A binder object guarding the lifecycle of this test network.
+     * @hide
+     */
+    public void setupTestNetwork(
+            @NonNull String iface, @NonNull int[] administratorUids, @NonNull IBinder binder) {
+        setupTestNetwork(iface, null, true, administratorUids, binder);
+    }
+
+    /**
+     * Create a tun interface for testing purposes
+     *
+     * @param linkAddrs an array of LinkAddresses to assign to the TUN interface
+     * @return A ParcelFileDescriptor of the underlying TUN interface. Close this to tear down the
+     *     TUN interface.
+     * @deprecated Use {@link #createTunInterface(Collection)} instead.
+     * @hide
+     */
+    @Deprecated
+    @NonNull
+    public TestNetworkInterface createTunInterface(@NonNull LinkAddress[] linkAddrs) {
+        return createTunInterface(Arrays.asList(linkAddrs));
+    }
+
+    /**
+     * Create a tun interface for testing purposes
+     *
+     * @param linkAddrs an array of LinkAddresses to assign to the TUN interface
+     * @return A ParcelFileDescriptor of the underlying TUN interface. Close this to tear down the
+     *     TUN interface.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @NonNull
+    public TestNetworkInterface createTunInterface(@NonNull Collection<LinkAddress> linkAddrs) {
+        try {
+            final LinkAddress[] arr = new LinkAddress[linkAddrs.size()];
+            return mService.createTunInterface(linkAddrs.toArray(arr));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Create a tap interface for testing purposes
+     *
+     * @return A ParcelFileDescriptor of the underlying TAP interface. Close this to tear down the
+     *     TAP interface.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @NonNull
+    public TestNetworkInterface createTapInterface() {
+        try {
+            return mService.createTapInterface();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+}
diff --git a/framework/src/android/net/TestNetworkSpecifier.java b/framework/src/android/net/TestNetworkSpecifier.java
new file mode 100644
index 0000000..117457d
--- /dev/null
+++ b/framework/src/android/net/TestNetworkSpecifier.java
@@ -0,0 +1,97 @@
+/*
+ * 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 android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * A {@link NetworkSpecifier} used to identify test interfaces.
+ *
+ * @see TestNetworkManager
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class TestNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+
+    /**
+     * Name of the network interface.
+     */
+    @NonNull
+    private final String mInterfaceName;
+
+    public TestNetworkSpecifier(@NonNull String interfaceName) {
+        if (TextUtils.isEmpty(interfaceName)) {
+            throw new IllegalArgumentException("Empty interfaceName");
+        }
+        mInterfaceName = interfaceName;
+    }
+
+    // This may be null in the future to support specifiers based on data other than the interface
+    // name.
+    @Nullable
+    public String getInterfaceName() {
+        return mInterfaceName;
+    }
+
+    @Override
+    public boolean canBeSatisfiedBy(@Nullable NetworkSpecifier other) {
+        return equals(other);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof TestNetworkSpecifier)) return false;
+        return TextUtils.equals(mInterfaceName, ((TestNetworkSpecifier) o).mInterfaceName);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mInterfaceName);
+    }
+
+    @Override
+    public String toString() {
+        return "TestNetworkSpecifier (" + mInterfaceName + ")";
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString(mInterfaceName);
+    }
+
+    public static final @NonNull Creator<TestNetworkSpecifier> CREATOR =
+            new Creator<TestNetworkSpecifier>() {
+        public TestNetworkSpecifier createFromParcel(Parcel in) {
+            return new TestNetworkSpecifier(in.readString());
+        }
+        public TestNetworkSpecifier[] newArray(int size) {
+            return new TestNetworkSpecifier[size];
+        }
+    };
+}
diff --git a/framework/src/android/net/TransportInfo.java b/framework/src/android/net/TransportInfo.java
new file mode 100644
index 0000000..fa889ea
--- /dev/null
+++ b/framework/src/android/net/TransportInfo.java
@@ -0,0 +1,75 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.SystemApi;
+
+/**
+ * A container for transport-specific capabilities which is returned by
+ * {@link NetworkCapabilities#getTransportInfo()}. Specific networks
+ * may provide concrete implementations of this interface.
+ * @see android.net.wifi.aware.WifiAwareNetworkInfo
+ * @see android.net.wifi.WifiInfo
+ */
+public interface TransportInfo {
+
+    /**
+     * Create a copy of a {@link TransportInfo} with some fields redacted based on the permissions
+     * held by the receiving app.
+     *
+     * <p>
+     * Usage by connectivity stack:
+     * <ul>
+     * <li> Connectivity stack will invoke {@link #getApplicableRedactions()} to find the list
+     * of redactions that are required by this {@link TransportInfo} instance.</li>
+     * <li> Connectivity stack then loops through each bit in the bitmask returned and checks if the
+     * receiving app holds the corresponding permission.
+     * <ul>
+     * <li> If the app holds the corresponding permission, the bit is cleared from the
+     * |redactions| bitmask. </li>
+     * <li> If the app does not hold the corresponding permission, the bit is retained in the
+     * |redactions| bitmask. </li>
+     * </ul>
+     * <li> Connectivity stack then invokes {@link #makeCopy(long)} with the necessary |redactions|
+     * to create a copy to send to the corresponding app. </li>
+     * </ul>
+     * </p>
+     *
+     * @param redactions bitmask of redactions that needs to be performed on this instance.
+     * @return Copy of this instance with the necessary redactions.
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @NonNull
+    default TransportInfo makeCopy(@NetworkCapabilities.RedactionType long redactions) {
+        return this;
+    }
+
+    /**
+     * Returns a bitmask of all the applicable redactions (based on the permissions held by the
+     * receiving app) to be performed on this TransportInfo.
+     *
+     * @return bitmask of redactions applicable on this instance.
+     * @see #makeCopy(long)
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    default @NetworkCapabilities.RedactionType long getApplicableRedactions() {
+        return NetworkCapabilities.REDACT_NONE;
+    }
+}
diff --git a/framework/src/android/net/UidRange.aidl b/framework/src/android/net/UidRange.aidl
new file mode 100644
index 0000000..f70fc8e
--- /dev/null
+++ b/framework/src/android/net/UidRange.aidl
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+/**
+ * An inclusive range of UIDs.
+ *
+ * {@hide}
+ */
+parcelable UidRange;
\ No newline at end of file
diff --git a/framework/src/android/net/UidRange.java b/framework/src/android/net/UidRange.java
new file mode 100644
index 0000000..bd33292
--- /dev/null
+++ b/framework/src/android/net/UidRange.java
@@ -0,0 +1,183 @@
+/*
+ * 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.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.Range;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * An inclusive range of UIDs.
+ *
+ * @hide
+ */
+public final class UidRange implements Parcelable {
+    public final int start;
+    public final int stop;
+
+    public UidRange(int startUid, int stopUid) {
+        if (startUid < 0) throw new IllegalArgumentException("Invalid start UID.");
+        if (stopUid < 0) throw new IllegalArgumentException("Invalid stop UID.");
+        if (startUid > stopUid) throw new IllegalArgumentException("Invalid UID range.");
+        start = startUid;
+        stop  = stopUid;
+    }
+
+    /** Creates a UidRange for the specified user. */
+    public static UidRange createForUser(UserHandle user) {
+        final UserHandle nextUser = UserHandle.of(user.getIdentifier() + 1);
+        final int start = user.getUid(0 /* appId */);
+        final int end = nextUser.getUid(0 /* appId */) - 1;
+        return new UidRange(start, end);
+    }
+
+    /** Returns the smallest user Id which is contained in this UidRange */
+    public int getStartUser() {
+        return UserHandle.getUserHandleForUid(start).getIdentifier();
+    }
+
+    /** Returns the largest user Id which is contained in this UidRange */
+    public int getEndUser() {
+        return UserHandle.getUserHandleForUid(stop).getIdentifier();
+    }
+
+    /** Returns whether the UidRange contains the specified UID. */
+    public boolean contains(int uid) {
+        return start <= uid && uid <= stop;
+    }
+
+    /**
+     * Returns the count of UIDs in this range.
+     */
+    public int count() {
+        return 1 + stop - start;
+    }
+
+    /**
+     * @return {@code true} if this range contains every UID contained by the {@code other} range.
+     */
+    public boolean containsRange(UidRange other) {
+        return start <= other.start && other.stop <= stop;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+        result = 31 * result + start;
+        result = 31 * result + stop;
+        return result;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o instanceof UidRange) {
+            UidRange other = (UidRange) o;
+            return start == other.start && stop == other.stop;
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return start + "-" + stop;
+    }
+
+    // Implement the Parcelable interface
+    // TODO: Consider making this class no longer parcelable, since all users are likely in the
+    // system server.
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(start);
+        dest.writeInt(stop);
+    }
+
+    public static final @android.annotation.NonNull Creator<UidRange> CREATOR =
+            new Creator<UidRange>() {
+        @Override
+        public UidRange createFromParcel(Parcel in) {
+            int start = in.readInt();
+            int stop = in.readInt();
+
+            return new UidRange(start, stop);
+        }
+        @Override
+        public UidRange[] newArray(int size) {
+            return new UidRange[size];
+        }
+    };
+
+    /**
+     * Returns whether any of the UidRange in the collection contains the specified uid
+     *
+     * @param ranges The collection of UidRange to check
+     * @param uid the uid in question
+     * @return {@code true} if the uid is contained within the ranges, {@code false} otherwise
+     *
+     * @see UidRange#contains(int)
+     */
+    public static boolean containsUid(Collection<UidRange> ranges, int uid) {
+        if (ranges == null) return false;
+        for (UidRange range : ranges) {
+            if (range.contains(uid)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     *  Convert a set of {@code Range<Integer>} to a set of {@link UidRange}.
+     */
+    @Nullable
+    public static ArraySet<UidRange> fromIntRanges(@Nullable Set<Range<Integer>> ranges) {
+        if (null == ranges) return null;
+
+        final ArraySet<UidRange> uids = new ArraySet<>();
+        for (Range<Integer> range : ranges) {
+            uids.add(new UidRange(range.getLower(), range.getUpper()));
+        }
+        return uids;
+    }
+
+    /**
+     *  Convert a set of {@link UidRange} to a set of {@code Range<Integer>}.
+     */
+    @Nullable
+    public static ArraySet<Range<Integer>> toIntRanges(@Nullable Set<UidRange> ranges) {
+        if (null == ranges) return null;
+
+        final ArraySet<Range<Integer>> uids = new ArraySet<>();
+        for (UidRange range : ranges) {
+            uids.add(new Range<Integer>(range.start, range.stop));
+        }
+        return uids;
+    }
+}
diff --git a/framework/src/android/net/VpnTransportInfo.java b/framework/src/android/net/VpnTransportInfo.java
new file mode 100644
index 0000000..4071c9a
--- /dev/null
+++ b/framework/src/android/net/VpnTransportInfo.java
@@ -0,0 +1,120 @@
+/*
+ * 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.REDACT_FOR_NETWORK_SETTINGS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.net.NetworkCapabilities.RedactionType;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * Container for VPN-specific transport information.
+ *
+ * @see android.net.TransportInfo
+ * @see NetworkCapabilities#getTransportInfo()
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public final class VpnTransportInfo implements TransportInfo, Parcelable {
+    /** Type of this VPN. */
+    private final int mType;
+
+    @Nullable
+    private final String mSessionId;
+
+    @Override
+    public @RedactionType long getApplicableRedactions() {
+        return REDACT_FOR_NETWORK_SETTINGS;
+    }
+
+    /**
+     * Create a copy of a {@link VpnTransportInfo} with the sessionId redacted if necessary.
+     */
+    @NonNull
+    public VpnTransportInfo makeCopy(@RedactionType long redactions) {
+        return new VpnTransportInfo(mType,
+            ((redactions & REDACT_FOR_NETWORK_SETTINGS) != 0) ? null : mSessionId);
+    }
+
+    public VpnTransportInfo(int type, @Nullable String sessionId) {
+        this.mType = type;
+        this.mSessionId = sessionId;
+    }
+
+    /**
+     * Returns the session Id of this VpnTransportInfo.
+     */
+    @Nullable
+    public String getSessionId() {
+        return mSessionId;
+    }
+
+    /**
+     * Returns the type of this VPN.
+     */
+    public int getType() {
+        return mType;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof VpnTransportInfo)) return false;
+
+        VpnTransportInfo that = (VpnTransportInfo) o;
+        return (this.mType == that.mType) && TextUtils.equals(this.mSessionId, that.mSessionId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mType, mSessionId);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("VpnTransportInfo{type=%d, sessionId=%s}", mType, mSessionId);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mType);
+        dest.writeString(mSessionId);
+    }
+
+    public static final @NonNull Creator<VpnTransportInfo> CREATOR =
+            new Creator<VpnTransportInfo>() {
+        public VpnTransportInfo createFromParcel(Parcel in) {
+            return new VpnTransportInfo(in.readInt(), in.readString());
+        }
+        public VpnTransportInfo[] newArray(int size) {
+            return new VpnTransportInfo[size];
+        }
+    };
+}
diff --git a/framework/src/android/net/apf/ApfCapabilities.java b/framework/src/android/net/apf/ApfCapabilities.java
new file mode 100644
index 0000000..663c1b3
--- /dev/null
+++ b/framework/src/android/net/apf/ApfCapabilities.java
@@ -0,0 +1,151 @@
+/*
+ * 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.apf;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityResources;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * APF program support capabilities. APF stands for Android Packet Filtering and it is a flexible
+ * way to drop unwanted network packets to save power.
+ *
+ * See documentation at hardware/google/apf/apf.h
+ *
+ * This class is immutable.
+ * @hide
+ */
+@SystemApi
+public final class ApfCapabilities implements Parcelable {
+    private static ConnectivityResources sResources;
+
+    /**
+     * Version of APF instruction set supported for packet filtering. 0 indicates no support for
+     * packet filtering using APF programs.
+     */
+    public final int apfVersionSupported;
+
+    /**
+     * Maximum size of APF program allowed.
+     */
+    public final int maximumApfProgramSize;
+
+    /**
+     * Format of packets passed to APF filter. Should be one of ARPHRD_*
+     */
+    public final int apfPacketFormat;
+
+    public ApfCapabilities(
+            int apfVersionSupported, int maximumApfProgramSize, int apfPacketFormat) {
+        this.apfVersionSupported = apfVersionSupported;
+        this.maximumApfProgramSize = maximumApfProgramSize;
+        this.apfPacketFormat = apfPacketFormat;
+    }
+
+    private ApfCapabilities(Parcel in) {
+        apfVersionSupported = in.readInt();
+        maximumApfProgramSize = in.readInt();
+        apfPacketFormat = in.readInt();
+    }
+
+    @NonNull
+    private static synchronized ConnectivityResources getResources(@NonNull Context ctx) {
+        if (sResources == null)  {
+            sResources = new ConnectivityResources(ctx);
+        }
+        return sResources;
+    }
+
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(apfVersionSupported);
+        dest.writeInt(maximumApfProgramSize);
+        dest.writeInt(apfPacketFormat);
+    }
+
+    public static final Creator<ApfCapabilities> CREATOR = new Creator<ApfCapabilities>() {
+        @Override
+        public ApfCapabilities createFromParcel(Parcel in) {
+            return new ApfCapabilities(in);
+        }
+
+        @Override
+        public ApfCapabilities[] newArray(int size) {
+            return new ApfCapabilities[size];
+        }
+    };
+
+    @NonNull
+    @Override
+    public String toString() {
+        return String.format("%s{version: %d, maxSize: %d, format: %d}", getClass().getSimpleName(),
+                apfVersionSupported, maximumApfProgramSize, apfPacketFormat);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof  ApfCapabilities)) return false;
+        final ApfCapabilities other = (ApfCapabilities) obj;
+        return apfVersionSupported == other.apfVersionSupported
+                && maximumApfProgramSize == other.maximumApfProgramSize
+                && apfPacketFormat == other.apfPacketFormat;
+    }
+
+    /**
+     * Determines whether the APF interpreter advertises support for the data buffer access opcodes
+     * LDDW (LoaD Data Word) and STDW (STore Data Word). Full LDDW (LoaD Data Word) and
+     * STDW (STore Data Word) support is present from APFv4 on.
+     *
+     * @return {@code true} if the IWifiStaIface#readApfPacketFilterData is supported.
+     */
+    public boolean hasDataAccess() {
+        return apfVersionSupported >= 4;
+    }
+
+    /**
+     * @return Whether the APF Filter in the device should filter out IEEE 802.3 Frames.
+     */
+    public static boolean getApfDrop8023Frames() {
+        // TODO: deprecate/remove this method (now unused in the platform), as the resource was
+        // moved to NetworkStack.
+        final Resources systemRes = Resources.getSystem();
+        final int id = systemRes.getIdentifier("config_apfDrop802_3Frames", "bool", "android");
+        return systemRes.getBoolean(id);
+    }
+
+    /**
+     * @return An array of denylisted EtherType, packets with EtherTypes within it will be dropped.
+     */
+    public static @NonNull int[] getApfEtherTypeBlackList() {
+        // TODO: deprecate/remove this method (now unused in the platform), as the resource was
+        // moved to NetworkStack.
+        final Resources systemRes = Resources.getSystem();
+        final int id = systemRes.getIdentifier("config_apfEthTypeBlackList", "array", "android");
+        return systemRes.getIntArray(id);
+    }
+}
diff --git a/framework/src/android/net/util/DnsUtils.java b/framework/src/android/net/util/DnsUtils.java
new file mode 100644
index 0000000..3fe245e
--- /dev/null
+++ b/framework/src/android/net/util/DnsUtils.java
@@ -0,0 +1,377 @@
+/*
+ * 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.util;
+
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.InetAddresses;
+import android.net.Network;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * @hide
+ */
+public class DnsUtils {
+    private static final String TAG = "DnsUtils";
+    private static final int CHAR_BIT = 8;
+    public static final int IPV6_ADDR_SCOPE_NODELOCAL = 0x01;
+    public static final int IPV6_ADDR_SCOPE_LINKLOCAL = 0x02;
+    public static final int IPV6_ADDR_SCOPE_SITELOCAL = 0x05;
+    public static final int IPV6_ADDR_SCOPE_GLOBAL = 0x0e;
+    private static final Comparator<SortableAddress> sRfc6724Comparator = new Rfc6724Comparator();
+
+    /**
+     * Comparator to sort SortableAddress in Rfc6724 style.
+     */
+    public static class Rfc6724Comparator implements Comparator<SortableAddress> {
+        // This function matches the behaviour of _rfc6724_compare in the native resolver.
+        @Override
+        public int compare(SortableAddress span1, SortableAddress span2) {
+            // Rule 1: Avoid unusable destinations.
+            if (span1.hasSrcAddr != span2.hasSrcAddr) {
+                return span2.hasSrcAddr - span1.hasSrcAddr;
+            }
+
+            // Rule 2: Prefer matching scope.
+            if (span1.scopeMatch != span2.scopeMatch) {
+                return span2.scopeMatch - span1.scopeMatch;
+            }
+
+            // TODO: Implement rule 3: Avoid deprecated addresses.
+            // TODO: Implement rule 4: Prefer home addresses.
+
+            // Rule 5: Prefer matching label.
+            if (span1.labelMatch != span2.labelMatch) {
+                return span2.labelMatch - span1.labelMatch;
+            }
+
+            // Rule 6: Prefer higher precedence.
+            if (span1.precedence != span2.precedence) {
+                return span2.precedence - span1.precedence;
+            }
+
+            // TODO: Implement rule 7: Prefer native transport.
+
+            // Rule 8: Prefer smaller scope.
+            if (span1.scope != span2.scope) {
+                return span1.scope - span2.scope;
+            }
+
+            // Rule 9: Use longest matching prefix. IPv6 only.
+            if (span1.prefixMatchLen != span2.prefixMatchLen) {
+                return span2.prefixMatchLen - span1.prefixMatchLen;
+            }
+
+            // Rule 10: Leave the order unchanged. Collections.sort is a stable sort.
+            return 0;
+        }
+    }
+
+    /**
+     * Class used to sort with RFC 6724
+     */
+    public static class SortableAddress {
+        public final int label;
+        public final int labelMatch;
+        public final int scope;
+        public final int scopeMatch;
+        public final int precedence;
+        public final int prefixMatchLen;
+        public final int hasSrcAddr;
+        public final InetAddress address;
+
+        public SortableAddress(@NonNull InetAddress addr, @Nullable InetAddress srcAddr) {
+            address = addr;
+            hasSrcAddr = (srcAddr != null) ? 1 : 0;
+            label = findLabel(addr);
+            scope = findScope(addr);
+            precedence = findPrecedence(addr);
+            labelMatch = ((srcAddr != null) && (label == findLabel(srcAddr))) ? 1 : 0;
+            scopeMatch = ((srcAddr != null) && (scope == findScope(srcAddr))) ? 1 : 0;
+            if (isIpv6Address(addr) && isIpv6Address(srcAddr)) {
+                prefixMatchLen = compareIpv6PrefixMatchLen(srcAddr, addr);
+            } else {
+                prefixMatchLen = 0;
+            }
+        }
+    }
+
+    /**
+     * Sort the given address list in RFC6724 order.
+     * Will leave the list unchanged if an error occurs.
+     *
+     * This function matches the behaviour of _rfc6724_sort in the native resolver.
+     */
+    public static @NonNull List<InetAddress> rfc6724Sort(@Nullable Network network,
+            @NonNull List<InetAddress> answers) {
+        final ArrayList<SortableAddress> sortableAnswerList = new ArrayList<>();
+        for (InetAddress addr : answers) {
+            sortableAnswerList.add(new SortableAddress(addr, findSrcAddress(network, addr)));
+        }
+
+        Collections.sort(sortableAnswerList, sRfc6724Comparator);
+
+        final List<InetAddress> sortedAnswers = new ArrayList<>();
+        for (SortableAddress ans : sortableAnswerList) {
+            sortedAnswers.add(ans.address);
+        }
+
+        return sortedAnswers;
+    }
+
+    private static @Nullable InetAddress findSrcAddress(@Nullable Network network,
+            @NonNull InetAddress addr) {
+        final int domain;
+        if (isIpv4Address(addr)) {
+            domain = AF_INET;
+        } else if (isIpv6Address(addr)) {
+            domain = AF_INET6;
+        } else {
+            return null;
+        }
+        final FileDescriptor socket;
+        try {
+            socket = Os.socket(domain, SOCK_DGRAM, IPPROTO_UDP);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "findSrcAddress:" + e.toString());
+            return null;
+        }
+        try {
+            if (network != null) network.bindSocket(socket);
+            Os.connect(socket, new InetSocketAddress(addr, 0));
+            return ((InetSocketAddress) Os.getsockname(socket)).getAddress();
+        } catch (IOException | ErrnoException e) {
+            return null;
+        } finally {
+            IoUtils.closeQuietly(socket);
+        }
+    }
+
+    /**
+     * Get the label for a given IPv4/IPv6 address.
+     * RFC 6724, section 2.1.
+     *
+     * Note that Java will return an IPv4-mapped address as an IPv4 address.
+     */
+    private static int findLabel(@NonNull InetAddress addr) {
+        if (isIpv4Address(addr)) {
+            return 4;
+        } else if (isIpv6Address(addr)) {
+            if (addr.isLoopbackAddress()) {
+                return 0;
+            } else if (isIpv6Address6To4(addr)) {
+                return 2;
+            } else if (isIpv6AddressTeredo(addr)) {
+                return 5;
+            } else if (isIpv6AddressULA(addr)) {
+                return 13;
+            } else if (((Inet6Address) addr).isIPv4CompatibleAddress()) {
+                return 3;
+            } else if (addr.isSiteLocalAddress()) {
+                return 11;
+            } else if (isIpv6Address6Bone(addr)) {
+                return 12;
+            } else {
+                // All other IPv6 addresses, including global unicast addresses.
+                return 1;
+            }
+        } else {
+            // This should never happen.
+            return 1;
+        }
+    }
+
+    private static boolean isIpv6Address(@Nullable InetAddress addr) {
+        return addr instanceof Inet6Address;
+    }
+
+    private static boolean isIpv4Address(@Nullable InetAddress addr) {
+        return addr instanceof Inet4Address;
+    }
+
+    private static boolean isIpv6Address6To4(@NonNull InetAddress addr) {
+        if (!isIpv6Address(addr)) return false;
+        final byte[] byteAddr = addr.getAddress();
+        return byteAddr[0] == 0x20 && byteAddr[1] == 0x02;
+    }
+
+    private static boolean isIpv6AddressTeredo(@NonNull InetAddress addr) {
+        if (!isIpv6Address(addr)) return false;
+        final byte[] byteAddr = addr.getAddress();
+        return byteAddr[0] == 0x20 && byteAddr[1] == 0x01 && byteAddr[2] == 0x00
+                && byteAddr[3] == 0x00;
+    }
+
+    private static boolean isIpv6AddressULA(@NonNull InetAddress addr) {
+        return isIpv6Address(addr) && (addr.getAddress()[0] & 0xfe) == 0xfc;
+    }
+
+    private static boolean isIpv6Address6Bone(@NonNull InetAddress addr) {
+        if (!isIpv6Address(addr)) return false;
+        final byte[] byteAddr = addr.getAddress();
+        return byteAddr[0] == 0x3f && byteAddr[1] == (byte) 0xfe;
+    }
+
+    private static int getIpv6MulticastScope(@NonNull InetAddress addr) {
+        return !isIpv6Address(addr) ? 0 : (addr.getAddress()[1] & 0x0f);
+    }
+
+    private static int findScope(@NonNull InetAddress addr) {
+        if (isIpv6Address(addr)) {
+            if (addr.isMulticastAddress()) {
+                return getIpv6MulticastScope(addr);
+            } else if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
+                /**
+                 * RFC 4291 section 2.5.3 says loopback is to be treated as having
+                 * link-local scope.
+                 */
+                return IPV6_ADDR_SCOPE_LINKLOCAL;
+            } else if (addr.isSiteLocalAddress()) {
+                return IPV6_ADDR_SCOPE_SITELOCAL;
+            } else {
+                return IPV6_ADDR_SCOPE_GLOBAL;
+            }
+        } else if (isIpv4Address(addr)) {
+            if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
+                return IPV6_ADDR_SCOPE_LINKLOCAL;
+            } else {
+                /**
+                 * RFC 6724 section 3.2. Other IPv4 addresses, including private addresses
+                 * and shared addresses (100.64.0.0/10), are assigned global scope.
+                 */
+                return IPV6_ADDR_SCOPE_GLOBAL;
+            }
+        } else {
+            /**
+             * This should never happen.
+             * Return a scope with low priority as a last resort.
+             */
+            return IPV6_ADDR_SCOPE_NODELOCAL;
+        }
+    }
+
+    /**
+     * Get the precedence for a given IPv4/IPv6 address.
+     * RFC 6724, section 2.1.
+     *
+     * Note that Java will return an IPv4-mapped address as an IPv4 address.
+     */
+    private static int findPrecedence(@NonNull InetAddress addr) {
+        if (isIpv4Address(addr)) {
+            return 35;
+        } else if (isIpv6Address(addr)) {
+            if (addr.isLoopbackAddress()) {
+                return 50;
+            } else if (isIpv6Address6To4(addr)) {
+                return 30;
+            } else if (isIpv6AddressTeredo(addr)) {
+                return 5;
+            } else if (isIpv6AddressULA(addr)) {
+                return 3;
+            } else if (((Inet6Address) addr).isIPv4CompatibleAddress() || addr.isSiteLocalAddress()
+                    || isIpv6Address6Bone(addr)) {
+                return 1;
+            } else {
+                // All other IPv6 addresses, including global unicast addresses.
+                return 40;
+            }
+        } else {
+            return 1;
+        }
+    }
+
+    /**
+     * Find number of matching initial bits between the two addresses.
+     */
+    private static int compareIpv6PrefixMatchLen(@NonNull InetAddress srcAddr,
+            @NonNull InetAddress dstAddr) {
+        final byte[] srcByte = srcAddr.getAddress();
+        final byte[] dstByte = dstAddr.getAddress();
+
+        // This should never happen.
+        if (srcByte.length != dstByte.length) return 0;
+
+        for (int i = 0; i < dstByte.length; ++i) {
+            if (srcByte[i] == dstByte[i]) {
+                continue;
+            }
+            int x = (srcByte[i] & 0xff) ^ (dstByte[i] & 0xff);
+            return i * CHAR_BIT + (Integer.numberOfLeadingZeros(x) - 24);  // Java ints are 32 bits
+        }
+        return dstByte.length * CHAR_BIT;
+    }
+
+    /**
+     * Check if given network has Ipv4 capability
+     * This function matches the behaviour of have_ipv4 in the native resolver.
+     */
+    public static boolean haveIpv4(@Nullable Network network) {
+        final SocketAddress addrIpv4 =
+                new InetSocketAddress(InetAddresses.parseNumericAddress("8.8.8.8"), 0);
+        return checkConnectivity(network, AF_INET, addrIpv4);
+    }
+
+    /**
+     * Check if given network has Ipv6 capability
+     * This function matches the behaviour of have_ipv6 in the native resolver.
+     */
+    public static boolean haveIpv6(@Nullable Network network) {
+        final SocketAddress addrIpv6 =
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2000::"), 0);
+        return checkConnectivity(network, AF_INET6, addrIpv6);
+    }
+
+    private static boolean checkConnectivity(@Nullable Network network,
+            int domain, @NonNull SocketAddress addr) {
+        final FileDescriptor socket;
+        try {
+            socket = Os.socket(domain, SOCK_DGRAM, IPPROTO_UDP);
+        } catch (ErrnoException e) {
+            return false;
+        }
+        try {
+            if (network != null) network.bindSocket(socket);
+            Os.connect(socket, addr);
+        } catch (IOException | ErrnoException e) {
+            return false;
+        } finally {
+            IoUtils.closeQuietly(socket);
+        }
+        return true;
+    }
+}
diff --git a/framework/src/android/net/util/KeepaliveUtils.java b/framework/src/android/net/util/KeepaliveUtils.java
new file mode 100644
index 0000000..8d7a0b3
--- /dev/null
+++ b/framework/src/android/net/util/KeepaliveUtils.java
@@ -0,0 +1,117 @@
+/*
+ * 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.util;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityResources;
+import android.net.NetworkCapabilities;
+import android.text.TextUtils;
+import android.util.AndroidRuntimeException;
+
+/**
+ * Collection of utilities for socket keepalive offload.
+ *
+ * @hide
+ */
+public final class KeepaliveUtils {
+
+    public static final String TAG = "KeepaliveUtils";
+
+    public static class KeepaliveDeviceConfigurationException extends AndroidRuntimeException {
+        public KeepaliveDeviceConfigurationException(final String msg) {
+            super(msg);
+        }
+    }
+
+    /**
+     * Read supported keepalive count for each transport type from overlay resource. This should be
+     * used to create a local variable store of resource customization, and use it as the input for
+     * {@link getSupportedKeepalivesForNetworkCapabilities}.
+     *
+     * @param context The context to read resource from.
+     * @return An array of supported keepalive count for each transport type.
+     */
+    @NonNull
+    public static int[] getSupportedKeepalives(@NonNull Context context) {
+        String[] res = null;
+        try {
+            final ConnectivityResources connRes = new ConnectivityResources(context);
+            // TODO: use R.id.config_networkSupportedKeepaliveCount directly
+            final int id = connRes.get().getIdentifier("config_networkSupportedKeepaliveCount",
+                    "array", connRes.getResourcesContext().getPackageName());
+            res = new ConnectivityResources(context).get().getStringArray(id);
+        } catch (Resources.NotFoundException unused) {
+        }
+        if (res == null) throw new KeepaliveDeviceConfigurationException("invalid resource");
+
+        final int[] ret = new int[NetworkCapabilities.MAX_TRANSPORT + 1];
+        for (final String row : res) {
+            if (TextUtils.isEmpty(row)) {
+                throw new KeepaliveDeviceConfigurationException("Empty string");
+            }
+            final String[] arr = row.split(",");
+            if (arr.length != 2) {
+                throw new KeepaliveDeviceConfigurationException("Invalid parameter length");
+            }
+
+            int transport;
+            int supported;
+            try {
+                transport = Integer.parseInt(arr[0]);
+                supported = Integer.parseInt(arr[1]);
+            } catch (NumberFormatException e) {
+                throw new KeepaliveDeviceConfigurationException("Invalid number format");
+            }
+
+            if (!NetworkCapabilities.isValidTransport(transport)) {
+                throw new KeepaliveDeviceConfigurationException("Invalid transport " + transport);
+            }
+
+            if (supported < 0) {
+                throw new KeepaliveDeviceConfigurationException(
+                        "Invalid supported count " + supported + " for "
+                                + NetworkCapabilities.transportNameOf(transport));
+            }
+            ret[transport] = supported;
+        }
+        return ret;
+    }
+
+    /**
+     * Get supported keepalive count for the given {@link NetworkCapabilities}.
+     *
+     * @param supportedKeepalives An array of supported keepalive count for each transport type.
+     * @param nc The {@link NetworkCapabilities} of the network the socket keepalive is on.
+     *
+     * @return Supported keepalive count for the given {@link NetworkCapabilities}.
+     */
+    public static int getSupportedKeepalivesForNetworkCapabilities(
+            @NonNull int[] supportedKeepalives, @NonNull NetworkCapabilities nc) {
+        final int[] transports = nc.getTransportTypes();
+        if (transports.length == 0) return 0;
+        int supportedCount = supportedKeepalives[transports[0]];
+        // Iterate through transports and return minimum supported value.
+        for (final int transport : transports) {
+            if (supportedCount > supportedKeepalives[transport]) {
+                supportedCount = supportedKeepalives[transport];
+            }
+        }
+        return supportedCount;
+    }
+}
diff --git a/framework/src/android/net/util/MultinetworkPolicyTracker.java b/framework/src/android/net/util/MultinetworkPolicyTracker.java
new file mode 100644
index 0000000..0b42a00
--- /dev/null
+++ b/framework/src/android/net/util/MultinetworkPolicyTracker.java
@@ -0,0 +1,247 @@
+/*
+ * 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.util;
+
+import static android.net.ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI;
+import static android.net.ConnectivitySettingsManager.NETWORK_METERED_MULTIPATH_PREFERENCE;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.net.ConnectivityResources;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * A class to encapsulate management of the "Smart Networking" capability of
+ * avoiding bad Wi-Fi when, for example upstream connectivity is lost or
+ * certain critical link failures occur.
+ *
+ * This enables the device to switch to another form of connectivity, like
+ * mobile, if it's available and working.
+ *
+ * The Runnable |avoidBadWifiCallback|, if given, is posted to the supplied
+ * Handler' whenever the computed "avoid bad wifi" value changes.
+ *
+ * Disabling this reverts the device to a level of networking sophistication
+ * circa 2012-13 by disabling disparate code paths each of which contribute to
+ * maintaining continuous, working Internet connectivity.
+ *
+ * @hide
+ */
+public class MultinetworkPolicyTracker {
+    private static String TAG = MultinetworkPolicyTracker.class.getSimpleName();
+
+    private final Context mContext;
+    private final ConnectivityResources mResources;
+    private final Handler mHandler;
+    private final Runnable mAvoidBadWifiCallback;
+    private final List<Uri> mSettingsUris;
+    private final ContentResolver mResolver;
+    private final SettingObserver mSettingObserver;
+    private final BroadcastReceiver mBroadcastReceiver;
+
+    private volatile boolean mAvoidBadWifi = true;
+    private volatile int mMeteredMultipathPreference;
+    private int mActiveSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+    // Mainline module can't use internal HandlerExecutor, so add an identical executor here.
+    private static class HandlerExecutor implements Executor {
+        @NonNull
+        private final Handler mHandler;
+
+        HandlerExecutor(@NonNull Handler handler) {
+            mHandler = handler;
+        }
+        @Override
+        public void execute(Runnable command) {
+            if (!mHandler.post(command)) {
+                throw new RejectedExecutionException(mHandler + " is shutting down");
+            }
+        }
+    }
+
+    @VisibleForTesting
+    protected class ActiveDataSubscriptionIdListener extends TelephonyCallback
+            implements TelephonyCallback.ActiveDataSubscriptionIdListener {
+        @Override
+        public void onActiveDataSubscriptionIdChanged(int subId) {
+            mActiveSubId = subId;
+            reevaluateInternal();
+        }
+    }
+
+    public MultinetworkPolicyTracker(Context ctx, Handler handler) {
+        this(ctx, handler, null);
+    }
+
+    public MultinetworkPolicyTracker(Context ctx, Handler handler, Runnable avoidBadWifiCallback) {
+        mContext = ctx;
+        mResources = new ConnectivityResources(ctx);
+        mHandler = handler;
+        mAvoidBadWifiCallback = avoidBadWifiCallback;
+        mSettingsUris = Arrays.asList(
+                Settings.Global.getUriFor(NETWORK_AVOID_BAD_WIFI),
+                Settings.Global.getUriFor(NETWORK_METERED_MULTIPATH_PREFERENCE));
+        mResolver = mContext.getContentResolver();
+        mSettingObserver = new SettingObserver();
+        mBroadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                reevaluateInternal();
+            }
+        };
+
+        ctx.getSystemService(TelephonyManager.class).registerTelephonyCallback(
+                new HandlerExecutor(handler), new ActiveDataSubscriptionIdListener());
+
+        updateAvoidBadWifi();
+        updateMeteredMultipathPreference();
+    }
+
+    public void start() {
+        for (Uri uri : mSettingsUris) {
+            mResolver.registerContentObserver(uri, false, mSettingObserver);
+        }
+
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+        mContext.registerReceiverForAllUsers(mBroadcastReceiver, intentFilter,
+                null /* broadcastPermission */, mHandler);
+
+        reevaluate();
+    }
+
+    public void shutdown() {
+        mResolver.unregisterContentObserver(mSettingObserver);
+
+        mContext.unregisterReceiver(mBroadcastReceiver);
+    }
+
+    public boolean getAvoidBadWifi() {
+        return mAvoidBadWifi;
+    }
+
+    // TODO: move this to MultipathPolicyTracker.
+    public int getMeteredMultipathPreference() {
+        return mMeteredMultipathPreference;
+    }
+
+    /**
+     * Whether the device or carrier configuration disables avoiding bad wifi by default.
+     */
+    public boolean configRestrictsAvoidBadWifi() {
+        // TODO: use R.integer.config_networkAvoidBadWifi directly
+        final int id = mResources.get().getIdentifier("config_networkAvoidBadWifi",
+                "integer", mResources.getResourcesContext().getPackageName());
+        return (getResourcesForActiveSubId().getInteger(id) == 0);
+    }
+
+    @NonNull
+    private Resources getResourcesForActiveSubId() {
+        return SubscriptionManager.getResourcesForSubId(
+                mResources.getResourcesContext(), mActiveSubId);
+    }
+
+    /**
+     * Whether we should display a notification when wifi becomes unvalidated.
+     */
+    public boolean shouldNotifyWifiUnvalidated() {
+        return configRestrictsAvoidBadWifi() && getAvoidBadWifiSetting() == null;
+    }
+
+    public String getAvoidBadWifiSetting() {
+        return Settings.Global.getString(mResolver, NETWORK_AVOID_BAD_WIFI);
+    }
+
+    @VisibleForTesting
+    public void reevaluate() {
+        mHandler.post(this::reevaluateInternal);
+    }
+
+    /**
+     * Reevaluate the settings. Must be called on the handler thread.
+     */
+    private void reevaluateInternal() {
+        if (updateAvoidBadWifi() && mAvoidBadWifiCallback != null) {
+            mAvoidBadWifiCallback.run();
+        }
+        updateMeteredMultipathPreference();
+    }
+
+    public boolean updateAvoidBadWifi() {
+        final boolean settingAvoidBadWifi = "1".equals(getAvoidBadWifiSetting());
+        final boolean prev = mAvoidBadWifi;
+        mAvoidBadWifi = settingAvoidBadWifi || !configRestrictsAvoidBadWifi();
+        return mAvoidBadWifi != prev;
+    }
+
+    /**
+     * The default (device and carrier-dependent) value for metered multipath preference.
+     */
+    public int configMeteredMultipathPreference() {
+        // TODO: use R.integer.config_networkMeteredMultipathPreference directly
+        final int id = mResources.get().getIdentifier("config_networkMeteredMultipathPreference",
+                "integer", mResources.getResourcesContext().getPackageName());
+        return mResources.get().getInteger(id);
+    }
+
+    public void updateMeteredMultipathPreference() {
+        String setting = Settings.Global.getString(mResolver, NETWORK_METERED_MULTIPATH_PREFERENCE);
+        try {
+            mMeteredMultipathPreference = Integer.parseInt(setting);
+        } catch (NumberFormatException e) {
+            mMeteredMultipathPreference = configMeteredMultipathPreference();
+        }
+    }
+
+    private class SettingObserver extends ContentObserver {
+        public SettingObserver() {
+            super(null);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            Log.wtf(TAG, "Should never be reached.");
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            if (!mSettingsUris.contains(uri)) {
+                Log.wtf(TAG, "Unexpected settings observation: " + uri);
+            }
+            reevaluate();
+        }
+    }
+}
diff --git a/service/Android.bp b/service/Android.bp
new file mode 100644
index 0000000..841e189
--- /dev/null
+++ b/service/Android.bp
@@ -0,0 +1,114 @@
+//
+// 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 {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_shared {
+    name: "libservice-connectivity",
+    min_sdk_version: "30",
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+    srcs: [
+        "jni/com_android_server_TestNetworkService.cpp",
+        "jni/onload.cpp",
+    ],
+    stl: "libc++_static",
+    header_libs: [
+        "libbase_headers",
+    ],
+    shared_libs: [
+        "liblog",
+        "libnativehelper",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
+java_library {
+    name: "service-connectivity-pre-jarjar",
+    sdk_version: "system_server_current",
+    min_sdk_version: "30",
+    srcs: [
+        "src/**/*.java",
+        ":framework-connectivity-shared-srcs",
+        ":services-connectivity-shared-srcs",
+        // TODO: move to net-utils-device-common, enable shrink optimization to avoid extra classes
+        ":net-module-utils-srcs",
+    ],
+    libs: [
+        // TODO (b/183097033) remove once system_server_current includes core_current
+        "stable.core.platform.api.stubs",
+        "android_system_server_stubs_current",
+        "framework-annotations-lib",
+        "framework-connectivity.impl",
+        "framework-tethering.stubs.module_lib",
+        "framework-wifi.stubs.module_lib",
+        "unsupportedappusage",
+        "ServiceConnectivityResources",
+    ],
+    static_libs: [
+        "dnsresolver_aidl_interface-V7-java",
+        "modules-utils-os",
+        "net-utils-device-common",
+        "net-utils-framework-common",
+        "netd-client",
+        "netlink-client",
+        "networkstack-client",
+        "PlatformProperties",
+        "service-connectivity-protos",
+    ],
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
+java_library {
+    name: "service-connectivity-protos",
+    sdk_version: "system_current",
+    min_sdk_version: "30",
+    proto: {
+        type: "nano",
+    },
+    srcs: [
+        ":system-messages-proto-src",
+    ],
+    libs: ["libprotobuf-java-nano"],
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
+
+java_library {
+    name: "service-connectivity",
+    sdk_version: "system_server_current",
+    min_sdk_version: "30",
+    installable: true,
+    static_libs: [
+        "service-connectivity-pre-jarjar",
+    ],
+    jarjar_rules: "jarjar-rules.txt",
+    apex_available: [
+        "com.android.tethering",
+    ],
+}
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
new file mode 100644
index 0000000..f491cc7
--- /dev/null
+++ b/service/ServiceConnectivityResources/Android.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.
+//
+
+// APK to hold all the wifi overlayable resources.
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "ServiceConnectivityResources",
+    sdk_version: "module_30",
+    min_sdk_version: "30",
+    resource_dirs: [
+        "res",
+    ],
+    privileged: true,
+    export_package_resources: true,
+    apex_available: [
+        "com.android.tethering",
+    ],
+    certificate: ":com.android.connectivity.resources.certificate",
+}
+
+android_app_certificate {
+    name: "com.android.connectivity.resources.certificate",
+    certificate: "resources-certs/com.android.connectivity.resources",
+}
diff --git a/service/ServiceConnectivityResources/AndroidManifest.xml b/service/ServiceConnectivityResources/AndroidManifest.xml
new file mode 100644
index 0000000..2c30302
--- /dev/null
+++ b/service/ServiceConnectivityResources/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<!-- Manifest for connectivity resources APK -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.connectivity.resources"
+          coreApp="true"
+          android:versionCode="1"
+          android:versionName="S-initial">
+    <application
+        android:label="@string/connectivityResourcesAppLabel"
+        android:defaultToDeviceProtectedStorage="true"
+        android:directBootAware="true">
+        <!-- This is only used to identify this app by resolving the action.
+             The activity is never actually triggered. -->
+        <activity android:name="android.app.Activity" android:exported="true" android:enabled="true">
+            <intent-filter>
+                <action android:name="com.android.server.connectivity.intent.action.SERVICE_CONNECTIVITY_RESOURCES_APK" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/service/ServiceConnectivityResources/res/drawable-hdpi/stat_notify_rssi_in_range.png b/service/ServiceConnectivityResources/res/drawable-hdpi/stat_notify_rssi_in_range.png
new file mode 100644
index 0000000..74977e6
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/drawable-hdpi/stat_notify_rssi_in_range.png
Binary files differ
diff --git a/service/ServiceConnectivityResources/res/drawable-mdpi/stat_notify_rssi_in_range.png b/service/ServiceConnectivityResources/res/drawable-mdpi/stat_notify_rssi_in_range.png
new file mode 100644
index 0000000..62e4fe9
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/drawable-mdpi/stat_notify_rssi_in_range.png
Binary files differ
diff --git a/service/ServiceConnectivityResources/res/drawable-xhdpi/stat_notify_rssi_in_range.png b/service/ServiceConnectivityResources/res/drawable-xhdpi/stat_notify_rssi_in_range.png
new file mode 100644
index 0000000..c0586d8
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/drawable-xhdpi/stat_notify_rssi_in_range.png
Binary files differ
diff --git a/service/ServiceConnectivityResources/res/drawable-xxhdpi/stat_notify_rssi_in_range.png b/service/ServiceConnectivityResources/res/drawable-xxhdpi/stat_notify_rssi_in_range.png
new file mode 100644
index 0000000..86c34ed
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/drawable-xxhdpi/stat_notify_rssi_in_range.png
Binary files differ
diff --git a/service/ServiceConnectivityResources/res/drawable/stat_notify_wifi_in_range.xml b/service/ServiceConnectivityResources/res/drawable/stat_notify_wifi_in_range.xml
new file mode 100644
index 0000000..a271ca5
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/drawable/stat_notify_wifi_in_range.xml
@@ -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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="26.0dp"
+        android:height="24.0dp"
+        android:viewportWidth="26.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#4DFFFFFF"
+        android:pathData="M19.1,14l-3.4,0l0,-1.5c0,-1.8 0.8,-2.8 1.5,-3.4C18.1,8.3 19.200001,8 20.6,8c1.2,0 2.3,0.3 3.1,0.8l1.9,-2.3C25.1,6.1 20.299999,2.1 13,2.1S0.9,6.1 0.4,6.5L13,22l0,0l0,0l0,0l0,0l6.5,-8.1L19.1,14z"/>
+    <path
+        android:fillColor="#FFFFFFFF"
+        android:pathData="M19.5,17.799999c0,-0.8 0.1,-1.3 0.2,-1.6c0.2,-0.3 0.5,-0.7 1.1,-1.2c0.4,-0.4 0.7,-0.8 1,-1.1s0.4,-0.8 0.4,-1.2c0,-0.5 -0.1,-0.9 -0.4,-1.2c-0.3,-0.3 -0.7,-0.4 -1.2,-0.4c-0.4,0 -0.8,0.1 -1.1,0.3c-0.3,0.2 -0.4,0.6 -0.4,1.1l-1.9,0c0,-1 0.3,-1.7 1,-2.2c0.6,-0.5 1.5,-0.8 2.5,-0.8c1.1,0 2,0.3 2.6,0.8c0.6,0.5 0.9,1.3 0.9,2.3c0,0.7 -0.2,1.3 -0.6,1.8c-0.4,0.6 -0.9,1.1 -1.5,1.6c-0.3,0.3 -0.5,0.5 -0.6,0.7c-0.1,0.2 -0.1,0.6 -0.1,1L19.5,17.700001zM21.4,21l-1.9,0l0,-1.8l1.9,0L21.4,21z"/>
+</vector>
diff --git a/service/ServiceConnectivityResources/res/values-af/strings.xml b/service/ServiceConnectivityResources/res/values-af/strings.xml
new file mode 100644
index 0000000..68720d5
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-af/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Meld aan by Wi-Fi-netwerk"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Meld by netwerk aan"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> het geen internettoegang nie"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Tik vir opsies"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Selnetwerk het nie internettoegang nie"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Netwerk het nie internettoegang nie"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Daar kan nie by private DNS-bediener ingegaan word nie"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> het beperkte konnektiwiteit"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Tik om in elk geval te koppel"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Het oorgeskakel na <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"\'n onbekende netwerktipe"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobiele data"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-am/strings.xml b/service/ServiceConnectivityResources/res/values-am/strings.xml
new file mode 100644
index 0000000..78d9283
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-am/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"ወደ Wi-Fi አውታረ መረብ በመለያ ግባ"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"ወደ አውታረ መረብ በመለያ ይግቡ"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ምንም የበይነ መረብ መዳረሻ የለም"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"ለአማራጮች መታ ያድርጉ"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"የተንቀሳቃሽ ስልክ አውታረ መረብ የበይነመረብ መዳረሻ የለውም"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"አውታረ መረብ የበይነመረብ መዳረሻ የለውም"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"የግል ዲኤንኤስ አገልጋይ ሊደረስበት አይችልም"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> የተገደበ ግንኙነት አለው"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"ለማንኛውም ለማገናኘት መታ ያድርጉ"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"ወደ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ተቀይሯል"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"ከ<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ወደ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ተቀይሯል"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"አንድ ያልታወቀ አውታረ መረብ ዓይነት"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"የተንቀሳቃሽ ስልክ ውሂብ"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"ብሉቱዝ"</item>
+    <item msgid="1616528372438698248">"ኤተርኔት"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ar/strings.xml b/service/ServiceConnectivityResources/res/values-ar/strings.xml
new file mode 100644
index 0000000..8698a7e
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ar/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"‏تسجيل الدخول إلى شبكة Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"تسجيل الدخول إلى الشبكة"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"لا يتوفّر في <xliff:g id="NETWORK_SSID">%1$s</xliff:g> إمكانية الاتصال بالإنترنت."</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"انقر للحصول على الخيارات."</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"شبكة الجوّال هذه غير متصلة بالإنترنت"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"الشبكة غير متصلة بالإنترنت"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"لا يمكن الوصول إلى خادم أسماء نظام نطاقات خاص"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"إمكانية اتصال <xliff:g id="NETWORK_SSID">%1$s</xliff:g> محدودة."</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"يمكنك النقر للاتصال على أي حال."</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"تم التبديل إلى <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"يستخدم الجهاز <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="501662047275723743">"تم التبديل من <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> إلى <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"نوع شبكة غير معروف"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"بيانات الجوّال"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"بلوتوث"</item>
+    <item msgid="1616528372438698248">"إيثرنت"</item>
+    <item msgid="9177085807664964627">"‏شبكة افتراضية خاصة (VPN)"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-as/strings.xml b/service/ServiceConnectivityResources/res/values-as/strings.xml
new file mode 100644
index 0000000..10b234a
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-as/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"ৱাই-ফাই নেটৱৰ্কত ছাইন ইন কৰক"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"নেটৱৰ্কত ছাইন ইন কৰক"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ৰ ইণ্টাৰনেটৰ এক্সেছ নাই"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"অধিক বিকল্পৰ বাবে টিপক"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"ম’বাইল নেটৱৰ্কৰ কোনো ইণ্টাৰনেটৰ এক্সেছ নাই"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"নেটৱৰ্কৰ কোনো ইণ্টাৰনেটৰ এক্সেছ নাই"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"ব্যক্তিগত DNS ছাৰ্ভাৰ এক্সেছ কৰিব নোৱাৰি"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ৰ সকলো সেৱাৰ এক্সেছ নাই"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"যিকোনো প্ৰকাৰে সংযোগ কৰিবলৈ টিপক"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>লৈ সলনি কৰা হ’ল"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"যেতিয়া <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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>ৰ পৰা <xliff:g id="NEW_NETWORK">%2$s</xliff:g> লৈ সলনি কৰা হ’ল"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"অজ্ঞাত প্ৰকাৰৰ নেটৱৰ্ক"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"ম’বাইল ডেটা"</item>
+    <item msgid="5520925862115353992">"ৱাই-ফাই"</item>
+    <item msgid="1055487873974272842">"ব্লুটুথ"</item>
+    <item msgid="1616528372438698248">"ইথাৰনেট"</item>
+    <item msgid="9177085807664964627">"ভিপিএন"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-az/strings.xml b/service/ServiceConnectivityResources/res/values-az/strings.xml
new file mode 100644
index 0000000..d75a204
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-az/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Wi-Fi şəbəkəsinə daxil ol"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Şəbəkəyə daxil olun"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> üçün internet girişi əlçatan deyil"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Seçimlər üçün tıklayın"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobil şəbəkənin internetə girişi yoxdur"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Şəbəkənin internetə girişi yoxdur"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Özəl DNS serverinə giriş mümkün deyil"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> bağlantını məhdudlaşdırdı"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"İstənilən halda klikləyin"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> şəbəkə növünə keçirildi"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<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="network_switch_type_name_unknown" msgid="3665696841646851068">"naməlum şəbəkə növü"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobil data"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml b/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..11e4957
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Prijavljivanje na WiFi mrežu"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Prijavite se na mrežu"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Dodirnite za opcije"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobilna mreža nema pristup internetu"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Mreža nema pristup internetu"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Pristup privatnom DNS serveru nije uspeo"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ima ograničenu vezu"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Dodirnite da biste se ipak povezali"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Prešli ste na tip mreže <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"nepoznat tip mreže"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobilni podaci"</item>
+    <item msgid="5520925862115353992">"WiFi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Eternet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-be/strings.xml b/service/ServiceConnectivityResources/res/values-be/strings.xml
new file mode 100644
index 0000000..6b0b1f1
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-be/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Уваход у сетку Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Увайдзіце ў сетку"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> не мае доступу ў інтэрнэт"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Дакраніцеся, каб убачыць параметры"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Мабільная сетка не мае доступу ў інтэрнэт"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Сетка не мае доступу ў інтэрнэт"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Не ўдалося атрымаць доступ да прыватнага DNS-сервера"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> мае абмежаваную магчымасць падключэння"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Націсніце, каб падключыцца"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Выкананы пераход да <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"Прылада выкарыстоўвае сетку <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="501662047275723743">"Выкананы пераход з <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> да <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"невядомы тып сеткі"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"мабільная перадача даных"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-bg/strings.xml b/service/ServiceConnectivityResources/res/values-bg/strings.xml
new file mode 100644
index 0000000..6427bd0
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-bg/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Влизане в Wi-Fi мрежа"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Вход в мрежата"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> няма достъп до интернет"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Докоснете за опции"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Мобилната мрежа няма достъп до интернет"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Мрежата няма достъп до интернет"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Не може да се осъществи достъп до частния DNS сървър"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> има ограничена свързаност"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Докоснете, за да се свържете въпреки това"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Превключи се към <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"Устройството използва <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="501662047275723743">"Превключи се от <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> към <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"неизвестен тип мрежа"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"мобилни данни"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"виртуална частна мрежа (VPN)"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-bn/strings.xml b/service/ServiceConnectivityResources/res/values-bn/strings.xml
new file mode 100644
index 0000000..74d4cd6
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-bn/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"ওয়াই-ফাই নেটওয়ার্কে সাইন-ইন করুন"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"নেটওয়ার্কে সাইন-ইন করুন"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-এর ইন্টারনেটে অ্যাক্সেস নেই"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"বিকল্পগুলির জন্য আলতো চাপুন"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"মোবাইল নেটওয়ার্কে কোনও ইন্টারনেট অ্যাক্সেস নেই"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"নেটওয়ার্কে কোনও ইন্টারনেট অ্যাক্সেস নেই"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"ব্যক্তিগত ডিএনএস সার্ভার অ্যাক্সেস করা যাবে না"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-এর সীমিত কানেক্টিভিটি আছে"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"তবুও কানেক্ট করতে ট্যাপ করুন"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> এ পাল্টানো হয়েছে"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> থেকে <xliff:g id="NEW_NETWORK">%2$s</xliff:g> এ পাল্টানো হয়েছে"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"এই নেটওয়ার্কের প্রকার অজানা"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"মোবাইল ডেটা"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"ব্লুটুথ"</item>
+    <item msgid="1616528372438698248">"ইথারনেট"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-bs/strings.xml b/service/ServiceConnectivityResources/res/values-bs/strings.xml
new file mode 100644
index 0000000..19f6e1a
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-bs/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Prijavljivanje na WiFi mrežu"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Prijava na mrežu"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"Mreža <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Dodirnite za opcije"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobilna mreža nema pristup internetu"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Mreža nema pristup internetu"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Nije moguće pristupiti privatnom DNS serveru"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"Mreža <xliff:g id="NETWORK_SSID">%1$s</xliff:g> ima ograničenu povezivost"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Dodirnite da se ipak povežete"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Prebačeno na: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"nepoznata vrsta mreže"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"prijenos podataka na mobilnoj mreži"</item>
+    <item msgid="5520925862115353992">"WiFi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ca/strings.xml b/service/ServiceConnectivityResources/res/values-ca/strings.xml
new file mode 100644
index 0000000..c55684d
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ca/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Inicia la sessió a la xarxa Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Inicia la sessió a la xarxa"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> no té accés a Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Toca per veure les opcions"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"La xarxa mòbil no té accés a Internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"La xarxa no té accés a Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"No es pot accedir al servidor DNS privat"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> té una connectivitat limitada"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Toca per connectar igualment"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Actualment en ús: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"una tipus de xarxa desconegut"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"dades mòbils"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-cs/strings.xml b/service/ServiceConnectivityResources/res/values-cs/strings.xml
new file mode 100644
index 0000000..fa8c411
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-cs/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Přihlásit se k síti Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Přihlásit se k síti"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"Síť <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nemá přístup k internetu"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Klepnutím zobrazíte možnosti"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobilní síť nemá přístup k internetu"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Síť nemá přístup k internetu"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Nelze získat přístup k soukromému serveru DNS"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"Síť <xliff:g id="NETWORK_SSID">%1$s</xliff:g> umožňuje jen omezené připojení"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Klepnutím se i přesto připojíte"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Přechod na síť <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"neznámý typ sítě"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobilní data"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-da/strings.xml b/service/ServiceConnectivityResources/res/values-da/strings.xml
new file mode 100644
index 0000000..f7be6df
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-da/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Log ind på Wi-Fi-netværk"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Log ind på netværk"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internetforbindelse"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Tryk for at se valgmuligheder"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobilnetværket har ingen internetadgang"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Netværket har ingen internetadgang"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Der er ikke adgang til den private DNS-server"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har begrænset forbindelse"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Tryk for at oprette forbindelse alligevel"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Der blev skiftet til <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"en ukendt netværkstype"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobildata"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-de/strings.xml b/service/ServiceConnectivityResources/res/values-de/strings.xml
new file mode 100644
index 0000000..1e7b80c
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-de/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"In WLAN anmelden"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Im Netzwerk anmelden"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> hat keinen Internetzugriff"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Für Optionen tippen"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobiles Netzwerk hat keinen Internetzugriff"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Netzwerk hat keinen Internetzugriff"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Auf den privaten DNS-Server kann nicht zugegriffen werden"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"Schlechte Verbindung mit <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Tippen, um die Verbindung trotzdem herzustellen"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Zu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> gewechselt"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Von \"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>\" zu \"<xliff:g id="NEW_NETWORK">%2$s</xliff:g>\" gewechselt"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"ein unbekannter Netzwerktyp"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"Mobile Daten"</item>
+    <item msgid="5520925862115353992">"WLAN"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-el/strings.xml b/service/ServiceConnectivityResources/res/values-el/strings.xml
new file mode 100644
index 0000000..89647fd
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-el/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Συνδεθείτε στο δίκτυο Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Σύνδεση στο δίκτυο"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"Η εφαρμογή <xliff:g id="NETWORK_SSID">%1$s</xliff:g> δεν έχει πρόσβαση στο διαδίκτυο"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Πατήστε για να δείτε τις επιλογές"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Το δίκτυο κινητής τηλεφωνίας δεν έχει πρόσβαση στο διαδίκτυο."</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Το δίκτυο δεν έχει πρόσβαση στο διαδίκτυο."</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Δεν είναι δυνατή η πρόσβαση στον ιδιωτικό διακομιστή DNS."</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"Το δίκτυο <xliff:g id="NETWORK_SSID">%1$s</xliff:g> έχει περιορισμένη συνδεσιμότητα"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Πατήστε για σύνδεση ούτως ή άλλως"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Μετάβαση σε δίκτυο <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"Η συσκευή χρησιμοποιεί το δίκτυο <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="501662047275723743">"Μετάβαση από το δίκτυο <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> στο δίκτυο <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"άγνωστος τύπος δικτύου"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"δεδομένα κινητής τηλεφωνίας"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml b/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..d29e2ec
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Sign in to a Wi-Fi network"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Sign in to network"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Tap for options"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobile network has no Internet access"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Network has no Internet access"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Private DNS server cannot be accessed"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Tap to connect anyway"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"an unknown network type"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobile data"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml b/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..d29e2ec
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Sign in to a Wi-Fi network"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Sign in to network"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Tap for options"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobile network has no Internet access"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Network has no Internet access"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Private DNS server cannot be accessed"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Tap to connect anyway"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"an unknown network type"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobile data"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml b/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..d29e2ec
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Sign in to a Wi-Fi network"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Sign in to network"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Tap for options"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobile network has no Internet access"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Network has no Internet access"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Private DNS server cannot be accessed"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Tap to connect anyway"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"an unknown network type"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobile data"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml b/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..d29e2ec
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Sign in to a Wi-Fi network"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Sign in to network"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Tap for options"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobile network has no Internet access"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Network has no Internet access"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Private DNS server cannot be accessed"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Tap to connect anyway"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"an unknown network type"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobile data"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml b/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..cd69133
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‎‏‎‏‎‎‏‎‎‏‏‏‎‎‎‏‏‏‎‎‏‏‏‎‎‏‎‏‎‏‎‏‎‏‏‏‏‎‎‏‎‏‎‏‎‏‏‎‏‎‏‏‏‎‎‏‎‏‎Sign in to Wi-Fi network‎‏‎‎‏‎"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‏‎‎‎‏‏‎‎‏‎‏‎‏‎‏‏‏‎‏‎‎‎‎‏‎‎‏‏‎‎‎‎‏‎‏‏‏‏‏‎‎‏‏‎‏‏‎‎‏‎‎‏‎‏‎‏‎‎Sign in to network‎‏‎‎‏‎"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‏‎‎‏‏‏‏‏‏‎‏‎‎‏‏‎‎‏‏‎‏‎‏‏‏‎‏‏‎‎‏‏‎‏‎‎‏‏‎‎‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‎‏‎‎‏‎‎‏‏‎<xliff:g id="NETWORK_SSID">%1$s</xliff:g>‎‏‎‎‏‏‏‎ has no internet access‎‏‎‎‏‎"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‎‎‎‎‏‎‎‏‎‎‏‏‎‏‎‏‎‎‎‎‎‏‏‎‏‎‎‎‏‏‏‏‎‏‎‏‎‏‎‎‏‏‏‏‎‎Tap for options‎‏‎‎‏‎"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‏‏‎‏‏‎‎‎‏‏‎‏‎‏‏‎‏‏‎‎‏‎‎‎‏‏‏‏‏‏‎‏‎‎‏‏‏‎‎‎‎‏‏‎‏‎‎‎‎‏‎‏‎‏‎Mobile network has no internet access‎‏‎‎‏‎"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‎‏‏‎‏‎‎‏‏‎‏‎‏‏‎‎‏‎‎‏‎‎‎‏‎‏‎‏‏‎‎‎‎‎‎‎‏‏‏‎‏‏‏‏‏‏‎‏‏‎‎‏‎Network has no internet access‎‏‎‎‏‎"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‏‎‏‏‏‏‎‏‎‎‏‏‎‎‏‎‎‏‏‏‏‏‎‏‎‏‎‏‏‎‏‏‏‎‏‏‎‎‏‏‏‎‎‎‏‎‎‏‎‏‎‎‎‎‏‏‏‎Private DNS server cannot be accessed‎‏‎‎‏‎"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‏‏‏‏‏‎‏‎‎‏‎‎‎‏‏‏‏‎‎‏‎‎‎‏‏‎‎‏‎‎‎‎‎‏‏‎‏‎‏‎‎‏‎‎‎‎‏‎‏‏‎‎‎‏‏‎‎‏‎‎‏‏‎<xliff:g id="NETWORK_SSID">%1$s</xliff:g>‎‏‎‎‏‏‏‎ has limited connectivity‎‏‎‎‏‎"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‏‎‏‎‏‏‎‏‎‏‎‎‏‏‎‎‏‎‏‏‎‏‏‎‎‎‎‎‎‎‎‏‏‏‎‎‎‎‏‏‎‎‏‎‎‏‏‎‎‏‏‎‎‎‎‎‎Tap to connect anyway‎‏‎‎‏‎"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‏‎‏‎‎‎‎‏‎‎‏‎‎‏‎‏‏‎‎‏‏‏‎‏‏‏‏‏‏‎‏‎‎‏‏‎‏‎‏‏‏‎‎‏‎‎‎‏‎‎‏‏‏‏‎‎‎‎Switched to ‎‏‎‎‏‏‎<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‏‏‎‏‏‎‎‏‏‎‏‎‎‎‏‏‎‎‎‎‎‏‏‎‎‏‎‎‏‏‏‏‏‎‎‎‏‎‎‎‏‎‏‏‎‏‎‏‏‎‎‏‎‎‏‎‎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="501662047275723743">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‏‎‏‏‏‏‎‏‏‎‎‏‎‎‎‎‏‎‏‏‏‏‏‎‎‏‎‏‎‏‏‏‎‏‎‎‏‎‏‎‏‎‎‎‏‎‏‎‏‏‏‏‎‏‏‏‏‏‎Switched from ‎‏‎‎‏‏‎<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>‎‏‎‎‏‏‏‎ to ‎‏‎‎‏‏‎<xliff:g id="NEW_NETWORK">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‎‏‏‏‏‏‎‎‏‎‏‎‏‏‏‏‎‏‏‎‏‏‎‎‎‏‎‏‎‎‏‎‏‎‎‎‎‏‏‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎an unknown network type‎‏‎‎‏‎"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‏‎‏‎‎‏‏‎‏‏‏‎‎‎‎‎‎‎‏‎‏‎‏‏‏‏‏‎‎‏‏‏‎‎‎‏‎‏‏‎‎‏‏‏‏‎‎‏‎‎‎‏‏‏‏‎‏‎mobile data‎‏‎‎‏‎"</item>
+    <item msgid="5520925862115353992">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‎‏‎‎‏‏‏‏‎‎‏‎‎‎‏‎‎‏‏‎‏‎‎‏‎‎‎‎‎‏‎‏‎‏‎‏‎‎‏‏‏‎‎‏‎‎‏‎‏‏‎‎‎‏‎‎‎‎Wi-Fi‎‏‎‎‏‎"</item>
+    <item msgid="1055487873974272842">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‏‎‏‎‎‏‎‏‏‏‎‏‏‎‎‎‏‎‏‎‎‎‎‏‎‎‎‏‎‎‎‎‏‏‏‏‏‎‎‎‎‎‎‎‎‎‏‏‎‏‎‎‏‎‏‎‎Bluetooth‎‏‎‎‏‎"</item>
+    <item msgid="1616528372438698248">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‏‎‎‏‏‎‏‏‏‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‎‎‎‎‏‎‎‎‏‏‎‎‏‎‎‎‏‎‎‏‎‎‎‎‏‎‎‎‎Ethernet‎‏‎‎‏‎"</item>
+    <item msgid="9177085807664964627">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‏‏‎‎‎‏‏‏‎‏‏‏‎‏‎‏‏‎‏‏‏‎‏‏‏‎‎‏‎‎‏‏‎‎‎‏‎‎‏‎‎‎‎‎‏‎‎‏‏‎VPN‎‏‎‎‏‎"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml b/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..9102dc0
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Accede a una red Wi-Fi."</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Acceder a la red"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>no tiene acceso a Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Presiona para ver opciones"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"La red móvil no tiene acceso a Internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"La red no tiene acceso a Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"No se puede acceder al servidor DNS privado"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiene conectividad limitada"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Presiona para conectarte de todas formas"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Se cambió a <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"un tipo de red desconocido"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"Datos móviles"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-es/strings.xml b/service/ServiceConnectivityResources/res/values-es/strings.xml
new file mode 100644
index 0000000..4c15566
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-es/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Iniciar sesión en red Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Iniciar sesión en la red"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> no tiene acceso a Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Toca para ver opciones"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"La red móvil no tiene acceso a Internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"La red no tiene acceso a Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"No se ha podido acceder al servidor DNS privado"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiene una conectividad limitada"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Toca para conectarte de todas formas"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Se ha cambiado a <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"tipo de red desconocido"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"datos móviles"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-et/strings.xml b/service/ServiceConnectivityResources/res/values-et/strings.xml
new file mode 100644
index 0000000..398223a
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-et/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Logi sisse WiFi-võrku"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Võrku sisselogimine"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"Võrgul <xliff:g id="NETWORK_SSID">%1$s</xliff:g> puudub Interneti-ühendus"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Puudutage valikute nägemiseks"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobiilsidevõrgul puudub Interneti-ühendus"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Võrgul puudub Interneti-ühendus"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Privaatsele DNS-serverile ei pääse juurde"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"Võrgu <xliff:g id="NETWORK_SSID">%1$s</xliff:g> ühendus on piiratud"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Puudutage, kui soovite siiski ühenduse luua"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Lülitati võrgule <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"tundmatu võrgutüüp"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobiilne andmeside"</item>
+    <item msgid="5520925862115353992">"WiFi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-eu/strings.xml b/service/ServiceConnectivityResources/res/values-eu/strings.xml
new file mode 100644
index 0000000..dd70316
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-eu/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Hasi saioa Wi-Fi sarean"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Hasi saioa sarean"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"Ezin da konektatu Internetera <xliff:g id="NETWORK_SSID">%1$s</xliff:g> sarearen bidez"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Sakatu aukerak ikusteko"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Sare mugikorra ezin da konektatu Internetera"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Sarea ezin da konektatu Internetera"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Ezin da atzitu DNS zerbitzari pribatua"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> sareak konektagarritasun murriztua du"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Sakatu hala ere konektatzeko"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> erabiltzen ari zara orain"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<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="network_switch_type_name_unknown" msgid="3665696841646851068">"sare mota ezezaguna"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"datu-konexioa"</item>
+    <item msgid="5520925862115353992">"Wifia"</item>
+    <item msgid="1055487873974272842">"Bluetooth-a"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-fa/strings.xml b/service/ServiceConnectivityResources/res/values-fa/strings.xml
new file mode 100644
index 0000000..46a946c
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-fa/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"‏ورود به شبکه Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"ورود به سیستم شبکه"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> به اینترنت دسترسی ندارد"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"برای گزینه‌ها ضربه بزنید"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"شبکه تلفن همراه به اینترنت دسترسی ندارد"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"شبکه به اینترنت دسترسی ندارد"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"‏سرور DNS خصوصی قابل دسترسی نیست"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> اتصال محدودی دارد"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"به‌هرصورت، برای اتصال ضربه بزنید"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"به <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> تغییر کرد"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"وقتی <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="501662047275723743">"از <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> به <xliff:g id="NEW_NETWORK">%2$s</xliff:g> تغییر کرد"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"نوع شبکه نامشخص"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"داده تلفن همراه"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"بلوتوث"</item>
+    <item msgid="1616528372438698248">"اترنت"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-fi/strings.xml b/service/ServiceConnectivityResources/res/values-fi/strings.xml
new file mode 100644
index 0000000..dd94441
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-fi/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Kirjaudu Wi-Fi-verkkoon"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Kirjaudu verkkoon"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ei ole yhteydessä internetiin"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Näytä vaihtoehdot napauttamalla."</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobiiliverkko ei ole yhteydessä internetiin"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Verkko ei ole yhteydessä internetiin"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Ei pääsyä yksityiselle DNS-palvelimelle"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> toimii rajoitetulla yhteydellä"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Yhdistä napauttamalla"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> otettiin käyttöön"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<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="network_switch_type_name_unknown" msgid="3665696841646851068">"tuntematon verkon tyyppi"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobiilidata"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml b/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..02ef50b
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Connectez-vous au réseau Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Connectez-vous au réseau"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"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="634938444133558942">"Touchez pour afficher les options"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Le réseau cellulaire n\'offre aucun accès à Internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Le réseau n\'offre aucun accès à Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Impossible d\'accéder au serveur DNS privé"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"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="5741329444564575840">"Touchez pour vous connecter quand même"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Passé au réseau <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"un type de réseau inconnu"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"données cellulaires"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"RPV"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-fr/strings.xml b/service/ServiceConnectivityResources/res/values-fr/strings.xml
new file mode 100644
index 0000000..08c9d81
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-fr/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Connectez-vous au réseau Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Se connecter au réseau"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"Aucune connexion à Internet pour <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Appuyez ici pour afficher des options."</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Le réseau mobile ne dispose d\'aucun accès à Internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Le réseau ne dispose d\'aucun accès à Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Impossible d\'accéder au serveur DNS privé"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"La connectivité de <xliff:g id="NETWORK_SSID">%1$s</xliff:g> est limitée"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Appuyer pour se connecter quand même"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Nouveau réseau : <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"type de réseau inconnu"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"données mobiles"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-gl/strings.xml b/service/ServiceConnectivityResources/res/values-gl/strings.xml
new file mode 100644
index 0000000..9f98055
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-gl/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Inicia sesión na rede wifi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Inicia sesión na rede"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> non ten acceso a Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Toca para ver opcións."</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"A rede de telefonía móbil non ten acceso a Internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"A rede non ten acceso a Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Non se puido acceder ao servidor DNS privado"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"A conectividade de <xliff:g id="NETWORK_SSID">%1$s</xliff:g> é limitada"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Toca para conectarte de todas formas"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Cambiouse a: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Cambiouse de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"un tipo de rede descoñecido"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"datos móbiles"</item>
+    <item msgid="5520925862115353992">"wifi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-gu/strings.xml b/service/ServiceConnectivityResources/res/values-gu/strings.xml
new file mode 100644
index 0000000..4ae5a2c
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-gu/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"વાઇ-ફાઇ નેટવર્ક પર સાઇન ઇન કરો"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"નેટવર્ક પર સાઇન ઇન કરો"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"વિકલ્પો માટે ટૅપ કરો"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"મોબાઇલ નેટવર્ક કોઈ ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"નેટવર્ક કોઈ ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"ખાનગી DNS સર્વર ઍક્સેસ કરી શકાતા નથી"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> મર્યાદિત કનેક્ટિવિટી ધરાવે છે"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"છતાં કનેક્ટ કરવા માટે ટૅપ કરો"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> પર સ્વિચ કર્યું"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"જ્યારે <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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> પરથી <xliff:g id="NEW_NETWORK">%2$s</xliff:g> પર સ્વિચ કર્યું"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"અજાણ્યો નેટવર્ક પ્રકાર"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"મોબાઇલ ડેટા"</item>
+    <item msgid="5520925862115353992">"વાઇ-ફાઇ"</item>
+    <item msgid="1055487873974272842">"બ્લૂટૂથ"</item>
+    <item msgid="1616528372438698248">"ઇથરનેટ"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-hi/strings.xml b/service/ServiceConnectivityResources/res/values-hi/strings.xml
new file mode 100644
index 0000000..eff1b60
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-hi/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"वाई-फ़ाई  नेटवर्क में साइन इन करें"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"नेटवर्क में साइन इन करें"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> का इंटरनेट नहीं चल रहा है"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"विकल्पों के लिए टैप करें"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"मोबाइल नेटवर्क पर इंटरनेट ऐक्सेस नहीं है"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"इस नेटवर्क पर इंटरनेट ऐक्सेस नहीं है"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"निजी डीएनएस सर्वर को ऐक्सेस नहीं किया जा सकता"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> की कनेक्टिविटी सीमित है"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"फिर भी कनेक्ट करने के लिए टैप करें"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> पर ले जाया गया"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> से <xliff:g id="NEW_NETWORK">%2$s</xliff:g> पर ले जाया गया"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"अज्ञात नेटवर्क प्रकार"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"मोबाइल डेटा"</item>
+    <item msgid="5520925862115353992">"वाई-फ़ाई"</item>
+    <item msgid="1055487873974272842">"ब्लूटूथ"</item>
+    <item msgid="1616528372438698248">"ईथरनेट"</item>
+    <item msgid="9177085807664964627">"वीपीएन"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-hr/strings.xml b/service/ServiceConnectivityResources/res/values-hr/strings.xml
new file mode 100644
index 0000000..52bfb93
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-hr/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Prijava na Wi-Fi mrežu"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Prijava na mrežu"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Dodirnite za opcije"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobilna mreža nema pristup internetu"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Mreža nema pristup internetu"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Nije moguće pristupiti privatnom DNS poslužitelju"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ima ograničenu povezivost"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Dodirnite da biste se ipak povezali"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Prelazak na drugu mrežu: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"nepoznata vrsta mreže"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobilni podaci"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-hu/strings.xml b/service/ServiceConnectivityResources/res/values-hu/strings.xml
new file mode 100644
index 0000000..2ff59b5
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-hu/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Bejelentkezés Wi-Fi hálózatba"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Bejelentkezés a hálózatba"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"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="634938444133558942">"Koppintson a beállítások megjelenítéséhez"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"A mobilhálózaton nincs internet-hozzáférés"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"A hálózaton nincs internet-hozzáférés"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"A privát DNS-kiszolgálóhoz nem lehet hozzáférni"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"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="5741329444564575840">"Koppintson, ha mindenképpen csatlakozni szeretne"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Átváltva erre: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"Á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="network_switch_type_name_unknown" msgid="3665696841646851068">"ismeretlen hálózati típus"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobiladatok"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-hy/strings.xml b/service/ServiceConnectivityResources/res/values-hy/strings.xml
new file mode 100644
index 0000000..b35d31c
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-hy/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Մուտք գործեք Wi-Fi ցանց"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Մուտք գործեք ցանց"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ցանցը չունի մուտք ինտերնետին"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Հպեք՝ ընտրանքները տեսնելու համար"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Բջջային ցանցը չի ապահովում ինտերնետ կապ"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Ցանցը միացված չէ ինտերնետին"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Մասնավոր DNS սերվերն անհասանելի է"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ցանցի կապը սահմանափակ է"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Հպեք՝ միանալու համար"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Անցել է <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ցանցի"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"Երբ <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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ցանցից անցել է <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ցանցի"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"ցանցի անհայտ տեսակ"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"բջջային ինտերնետ"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-in/strings.xml b/service/ServiceConnectivityResources/res/values-in/strings.xml
new file mode 100644
index 0000000..27d7d89
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-in/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Login ke jaringan Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Login ke jaringan"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tidak memiliki akses internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Ketuk untuk melihat opsi"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Jaringan seluler tidak memiliki akses internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Jaringan tidak memiliki akses internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Server DNS pribadi tidak dapat diakses"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> memiliki konektivitas terbatas"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Ketuk untuk tetap menyambungkan"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Dialihkan ke <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Dialihkan dari <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ke <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"jenis jaringan yang tidak dikenal"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"data seluler"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-is/strings.xml b/service/ServiceConnectivityResources/res/values-is/strings.xml
new file mode 100644
index 0000000..97f42dc
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-is/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Skrá inn á Wi-Fi net"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Skrá inn á net"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> er ekki með internetaðgang"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Ýttu til að sjá valkosti"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Farsímakerfið er ekki tengt við internetið"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Netkerfið er ekki tengt við internetið"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Ekki næst í DNS-einkaþjón"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"Tengigeta <xliff:g id="NETWORK_SSID">%1$s</xliff:g> er takmörkuð"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Ýttu til að tengjast samt"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Skipt yfir á <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Skipt úr <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> yfir í <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"óþekkt tegund netkerfis"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"farsímagögn"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-it/strings.xml b/service/ServiceConnectivityResources/res/values-it/strings.xml
new file mode 100644
index 0000000..7836101
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-it/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Accedi a rete Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Accedi alla rete"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> non ha accesso a Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Tocca per le opzioni"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"La rete mobile non ha accesso a Internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"La rete non ha accesso a Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Non è possibile accedere al server DNS privato"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ha una connettività limitata"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Tocca per connettere comunque"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Passato a <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Passato da <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"tipo di rete sconosciuto"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"dati mobili"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-iw/strings.xml b/service/ServiceConnectivityResources/res/values-iw/strings.xml
new file mode 100644
index 0000000..f322b99
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-iw/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"‏היכנס לרשת Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"היכנס לרשת"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"ל-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> אין גישה לאינטרנט"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"הקש לקבלת אפשרויות"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"לרשת הסלולרית אין גישה לאינטרנט"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"לרשת אין גישה לאינטרנט"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"‏לא ניתן לגשת לשרת DNS הפרטי"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"הקישוריות של <xliff:g id="NETWORK_SSID">%1$s</xliff:g> מוגבלת"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"כדי להתחבר למרות זאת יש להקיש"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"מעבר אל <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"המכשיר משתמש ברשת <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="501662047275723743">"עבר מרשת <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> לרשת <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"סוג רשת לא מזוהה"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"חבילת גלישה"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ja/strings.xml b/service/ServiceConnectivityResources/res/values-ja/strings.xml
new file mode 100644
index 0000000..1aa3216
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ja/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Wi-Fiネットワークにログイン"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"ネットワークにログインしてください"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> はインターネットにアクセスできません"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"タップしてその他のオプションを表示"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"モバイル ネットワークがインターネットに接続されていません"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"ネットワークがインターネットに接続されていません"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"プライベート DNS サーバーにアクセスできません"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> の接続が制限されています"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"接続するにはタップしてください"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"「<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>」に切り替えました"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"デバイスで「<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="501662047275723743">"「<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>」から「<xliff:g id="NEW_NETWORK">%2$s</xliff:g>」に切り替えました"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"不明なネットワーク タイプ"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"モバイルデータ"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"イーサネット"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ka/strings.xml b/service/ServiceConnectivityResources/res/values-ka/strings.xml
new file mode 100644
index 0000000..61d21b5
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ka/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Wi-Fi ქსელთან დაკავშირება"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"ქსელში შესვლა"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-ს არ აქვს ინტერნეტზე წვდომა"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"შეეხეთ ვარიანტების სანახავად"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"მობილურ ქსელს არ აქვს ინტერნეტზე წვდომა"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"ქსელს არ აქვს ინტერნეტზე წვდომა"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"პირად DNS სერვერზე წვდომა შეუძლებელია"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-ის კავშირები შეზღუდულია"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"შეეხეთ, თუ მაინც გსურთ დაკავშირება"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"ახლა გამოიყენება <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"თუ <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="501662047275723743">"ახლა გამოიყენება <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> (გამოიყენებოდა <xliff:g id="NEW_NETWORK">%2$s</xliff:g>)"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"უცნობი ტიპის ქსელი"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"მობილური ინტერნეტი"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-kk/strings.xml b/service/ServiceConnectivityResources/res/values-kk/strings.xml
new file mode 100644
index 0000000..969aaef
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-kk/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Wi-Fi желісіне кіру"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Желіге кіру"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> желісінің интернетті пайдалану мүмкіндігі шектеулі."</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Опциялар үшін түртіңіз"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Мобильдік желі интернетке қосылмаған."</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Желі интернетке қосылмаған."</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Жеке DNS серверіне кіру мүмкін емес."</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> желісінің қосылу мүмкіндігі шектеулі."</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Бәрібір жалғау үшін түртіңіз."</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> желісіне ауысты"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"Құрылғы <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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> желісінен <xliff:g id="NEW_NETWORK">%2$s</xliff:g> желісіне ауысты"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"желі түрі белгісіз"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"мобильдік деректер"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-km/strings.xml b/service/ServiceConnectivityResources/res/values-km/strings.xml
new file mode 100644
index 0000000..da3c337
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-km/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"ចូល​បណ្ដាញ​វ៉ាយហ្វាយ"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"ចូលទៅបណ្តាញ"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> មិនមាន​ការតភ្ជាប់អ៊ីនធឺណិត​ទេ"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"ប៉ះសម្រាប់ជម្រើស"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"បណ្ដាញ​ទូរសព្ទ​ចល័ត​មិនមានការតភ្ជាប់​អ៊ីនធឺណិតទេ"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"បណ្ដាញ​មិនមាន​ការតភ្ជាប់​អ៊ីនធឺណិតទេ"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"មិនអាច​ចូលប្រើ​ម៉ាស៊ីនមេ DNS ឯកជន​បានទេ"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> មានការតភ្ជាប់​មានកម្រិត"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"មិន​អី​ទេ ចុច​​ភ្ជាប់​ចុះ"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"បានប្តូរទៅ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"ឧបករណ៍​ប្រើ <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="501662047275723743">"បានប្តូរពី <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ទៅ <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"ប្រភេទបណ្តាញមិនស្គាល់"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"ទិន្នន័យ​ទូរសព្ទចល័ត"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"ប៊្លូធូស"</item>
+    <item msgid="1616528372438698248">"អ៊ីសឺរណិត"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-kn/strings.xml b/service/ServiceConnectivityResources/res/values-kn/strings.xml
new file mode 100644
index 0000000..3b5e047
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-kn/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"ವೈ-ಫೈ ನೆಟ್‍ವರ್ಕ್‌ಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"ನೆಟ್‌ವರ್ಕ್‌ಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಸಂಪರ್ಕವನ್ನು ಹೊಂದಿಲ್ಲ"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"ಆಯ್ಕೆಗಳಿಗೆ ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"ಮೊಬೈಲ್ ನೆಟ್‌ವರ್ಕ್‌ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿಲ್ಲ"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"ನೆಟ್‌ವರ್ಕ್‌ ಇಂಟರ್ನೆಟ್‌ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿಲ್ಲ"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"ಖಾಸಗಿ DNS ಸರ್ವರ್ ಅನ್ನು ಪ್ರವೇಶಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ಸೀಮಿತ ಸಂಪರ್ಕ ಕಲ್ಪಿಸುವಿಕೆಯನ್ನು ಹೊಂದಿದೆ"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"ಹೇಗಾದರೂ ಸಂಪರ್ಕಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ರಿಂದ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"ಅಪರಿಚಿತ ನೆಟ್‌ವರ್ಕ್ ಪ್ರಕಾರ"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"ಮೊಬೈಲ್ ಡೇಟಾ"</item>
+    <item msgid="5520925862115353992">"ವೈ-ಫೈ"</item>
+    <item msgid="1055487873974272842">"ಬ್ಲೂಟೂತ್‌"</item>
+    <item msgid="1616528372438698248">"ಇಥರ್ನೆಟ್"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ko/strings.xml b/service/ServiceConnectivityResources/res/values-ko/strings.xml
new file mode 100644
index 0000000..874bd75
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ko/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Wi-Fi 네트워크에 로그인"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"네트워크에 로그인"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>이(가) 인터넷에 액세스할 수 없습니다."</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"탭하여 옵션 보기"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"모바일 네트워크에 인터넷이 연결되어 있지 않습니다."</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"네트워크에 인터넷이 연결되어 있지 않습니다."</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"비공개 DNS 서버에 액세스할 수 없습니다."</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>에서 연결을 제한했습니다."</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"계속 연결하려면 탭하세요."</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>(으)로 전환"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>에서 <xliff:g id="NEW_NETWORK">%2$s</xliff:g>(으)로 전환"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"알 수 없는 네트워크 유형"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"모바일 데이터"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"블루투스"</item>
+    <item msgid="1616528372438698248">"이더넷"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ky/strings.xml b/service/ServiceConnectivityResources/res/values-ky/strings.xml
new file mode 100644
index 0000000..1ace4dc
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ky/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Wi-Fi түйүнүнө кирүү"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Тармакка кирүү"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> Интернетке туташуусу жок"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Параметрлерди ачуу үчүн таптап коюңуз"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Мобилдик Интернет жок"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Тармактын Интернет жок"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Жеке DNS сервери жеткиликсиз"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> байланышы чектелген"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Баары бир туташуу үчүн таптаңыз"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> тармагына которуштурулду"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> дегенден <xliff:g id="NEW_NETWORK">%2$s</xliff:g> тармагына которуштурулду"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"белгисиз тармак түрү"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"мобилдик трафик"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-lo/strings.xml b/service/ServiceConnectivityResources/res/values-lo/strings.xml
new file mode 100644
index 0000000..3db497e
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-lo/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"ເຂົ້າສູ່ລະບົບເຄືອຂ່າຍ Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"ລົງຊື່ເຂົ້າເຄືອຂ່າຍ"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ບໍ່ມີການເຊື່ອມຕໍ່ອິນເຕີເນັດ"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"ແຕະເພື່ອເບິ່ງຕົວເລືອກ"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"ເຄືອຂ່າຍມືຖືບໍ່ສາມາດເຂົ້າເຖິງອິນເຕີເນັດໄດ້"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"ເຄືອຂ່າຍບໍ່ສາມາດເຂົ້າເຖິງອິນເຕີເນັດໄດ້"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"ບໍ່ສາມາດເຂົ້າເຖິງເຊີບເວີ DNS ສ່ວນຕົວໄດ້"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ມີການເຊື່ອມຕໍ່ທີ່ຈຳກັດ"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"ແຕະເພື່ອຢືນຢັນການເຊື່ອມຕໍ່"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"ສະຫຼັບໄປໃຊ້ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ແລ້ວ"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"ອຸປະກອນຈະໃຊ້ <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="501662047275723743">"ສະຫຼັບຈາກ <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ໄປໃຊ້ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ແລ້ວ"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"ບໍ່ຮູ້ຈັກປະເພດເຄືອຂ່າຍ"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"ອິນເຕີເນັດມືຖື"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"ອີເທີເນັດ"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-lt/strings.xml b/service/ServiceConnectivityResources/res/values-lt/strings.xml
new file mode 100644
index 0000000..c639c1f
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-lt/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Prisijungti prie „Wi-Fi“ tinklo"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Prisijungti prie tinklo"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"„<xliff:g id="NETWORK_SSID">%1$s</xliff:g>“ negali pasiekti interneto"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Palieskite, kad būtų rodomos parinktys."</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobiliojo ryšio tinkle nėra prieigos prie interneto"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Tinkle nėra prieigos prie interneto"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Privataus DNS serverio negalima pasiekti"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"„<xliff:g id="NETWORK_SSID">%1$s</xliff:g>“ ryšys apribotas"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Palieskite, jei vis tiek norite prisijungti"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Perjungta į tinklą <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"Į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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"nežinomas tinklo tipas"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobiliojo ryšio duomenys"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Eternetas"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-lv/strings.xml b/service/ServiceConnectivityResources/res/values-lv/strings.xml
new file mode 100644
index 0000000..5774603
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-lv/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Pierakstieties Wi-Fi tīklā"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Pierakstīšanās tīklā"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"Tīklā <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nav piekļuves internetam"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Pieskarieties, lai skatītu iespējas."</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobilajā tīklā nav piekļuves internetam."</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Tīklā nav piekļuves internetam."</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Nevar piekļūt privātam DNS serverim."</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"Tīklā <xliff:g id="NETWORK_SSID">%1$s</xliff:g> ir ierobežota savienojamība"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Lai tik un tā izveidotu savienojumu, pieskarieties"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Pārslēdzās uz tīklu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"nezināms tīkla veids"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobilie dati"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-mcc204-mnc04/config.xml b/service/ServiceConnectivityResources/res/values-mcc204-mnc04/config.xml
new file mode 100644
index 0000000..7e7025f
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-mcc204-mnc04/config.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- Configuration values for ConnectivityService
+     DO NOT EDIT THIS FILE for specific device configuration; instead, use a Runtime Resources
+     Overlay package following the overlayable.xml configuration in the same directory:
+     https://source.android.com/devices/architecture/rros -->
+<resources>
+    <!-- Whether the device should automatically switch away from Wi-Fi networks that lose
+         Internet access. Actual device behaviour is controlled by
+         Settings.Global.NETWORK_AVOID_BAD_WIFI. This is the default value of that setting. -->
+    <integer translatable="false" name="config_networkAvoidBadWifi">0</integer>
+</resources>
\ No newline at end of file
diff --git a/service/ServiceConnectivityResources/res/values-mcc310-mnc004/config.xml b/service/ServiceConnectivityResources/res/values-mcc310-mnc004/config.xml
new file mode 100644
index 0000000..7e7025f
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-mcc310-mnc004/config.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- Configuration values for ConnectivityService
+     DO NOT EDIT THIS FILE for specific device configuration; instead, use a Runtime Resources
+     Overlay package following the overlayable.xml configuration in the same directory:
+     https://source.android.com/devices/architecture/rros -->
+<resources>
+    <!-- Whether the device should automatically switch away from Wi-Fi networks that lose
+         Internet access. Actual device behaviour is controlled by
+         Settings.Global.NETWORK_AVOID_BAD_WIFI. This is the default value of that setting. -->
+    <integer translatable="false" name="config_networkAvoidBadWifi">0</integer>
+</resources>
\ No newline at end of file
diff --git a/service/ServiceConnectivityResources/res/values-mcc311-mnc480/config.xml b/service/ServiceConnectivityResources/res/values-mcc311-mnc480/config.xml
new file mode 100644
index 0000000..7e7025f
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-mcc311-mnc480/config.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- Configuration values for ConnectivityService
+     DO NOT EDIT THIS FILE for specific device configuration; instead, use a Runtime Resources
+     Overlay package following the overlayable.xml configuration in the same directory:
+     https://source.android.com/devices/architecture/rros -->
+<resources>
+    <!-- Whether the device should automatically switch away from Wi-Fi networks that lose
+         Internet access. Actual device behaviour is controlled by
+         Settings.Global.NETWORK_AVOID_BAD_WIFI. This is the default value of that setting. -->
+    <integer translatable="false" name="config_networkAvoidBadWifi">0</integer>
+</resources>
\ No newline at end of file
diff --git a/service/ServiceConnectivityResources/res/values-mk/strings.xml b/service/ServiceConnectivityResources/res/values-mk/strings.xml
new file mode 100644
index 0000000..053c7cf
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-mk/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Најавете се на мрежа на Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Најавете се на мрежа"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> нема интернет-пристап"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Допрете за опции"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Мобилната мрежа нема интернет-пристап"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Мрежата нема интернет-пристап"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Не може да се пристапи до приватниот DNS-сервер"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> има ограничена поврзливост"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Допрете за да се поврзете и покрај тоа"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Префрлено на <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"Уредот користи <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="501662047275723743">"Префрлено од <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> на <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"непознат тип мрежа"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"мобилен интернет"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Етернет"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ml/strings.xml b/service/ServiceConnectivityResources/res/values-ml/strings.xml
new file mode 100644
index 0000000..3e80cf1
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ml/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"വൈഫൈ നെറ്റ്‌വർക്കിലേക്ക് സൈൻ ഇൻ ചെയ്യുക"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"നെറ്റ്‌വർക്കിലേക്ക് സൈൻ ഇൻ ചെയ്യുക"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> എന്നതിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"ഓപ്ഷനുകൾക്ക് ടാപ്പുചെയ്യുക"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"മൊബെെൽ നെറ്റ്‌വർക്കിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"നെറ്റ്‌വർക്കിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"സ്വകാര്യ DNS സെർവർ ആക്‌സസ് ചെയ്യാനാവില്ല"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> എന്നതിന് പരിമിതമായ കണക്റ്റിവിറ്റി ഉണ്ട്"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"ഏതുവിധേനയും കണക്‌റ്റ് ചെയ്യാൻ ടാപ്പ് ചെയ്യുക"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> എന്നതിലേക്ക് മാറി"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> നെറ്റ്‌വർക്കിൽ നിന്ന് <xliff:g id="NEW_NETWORK">%2$s</xliff:g> നെറ്റ്‌വർക്കിലേക്ക് മാറി"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"തിരിച്ചറിയാനാകാത്ത ഒരു നെറ്റ്‌വർക്ക് തരം"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"മൊബൈൽ ഡാറ്റ"</item>
+    <item msgid="5520925862115353992">"വൈഫൈ"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"ഇതര്‍നെറ്റ്"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-mn/strings.xml b/service/ServiceConnectivityResources/res/values-mn/strings.xml
new file mode 100644
index 0000000..214fc0c
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-mn/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Wi-Fi сүлжээнд нэвтэрнэ үү"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Сүлжээнд нэвтэрнэ үү"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-д интернэтийн хандалт алга"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Сонголт хийхийн тулд товшино уу"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Мобайл сүлжээнд интернэт хандалт байхгүй байна"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Сүлжээнд интернэт хандалт байхгүй байна"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Хувийн DNS серверт хандах боломжгүй байна"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> зарим үйлчилгээнд хандах боломжгүй байна"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Ямар ч тохиолдолд холбогдохын тулд товших"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> руу шилжүүлсэн"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>-с <xliff:g id="NEW_NETWORK">%2$s</xliff:g> руу шилжүүлсэн"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"сүлжээний тодорхойгүй төрөл"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"мобайл дата"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Этернэт"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-mr/strings.xml b/service/ServiceConnectivityResources/res/values-mr/strings.xml
new file mode 100644
index 0000000..c4b1998
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-mr/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"वाय-फाय नेटवर्कमध्‍ये साइन इन करा"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"नेटवर्कवर साइन इन करा"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ला इंटरनेट अ‍ॅक्सेस नाही"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"पर्यायांसाठी टॅप करा"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"मोबाइल नेटवर्कला इंटरनेट ॲक्सेस नाही"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"नेटवर्कला इंटरनेट ॲक्सेस नाही"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"खाजगी DNS सर्व्हर ॲक्सेस करू शकत नाही"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ला मर्यादित कनेक्टिव्हिटी आहे"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"तरीही कनेक्ट करण्यासाठी टॅप करा"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> वर स्विच केले"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> वरून <xliff:g id="NEW_NETWORK">%2$s</xliff:g> वर स्विच केले"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"अज्ञात नेटवर्क प्रकार"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"मोबाइल डेटा"</item>
+    <item msgid="5520925862115353992">"वाय-फाय"</item>
+    <item msgid="1055487873974272842">"ब्लूटूथ"</item>
+    <item msgid="1616528372438698248">"इथरनेट"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ms/strings.xml b/service/ServiceConnectivityResources/res/values-ms/strings.xml
new file mode 100644
index 0000000..529d0bd
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ms/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Log masuk ke rangkaian Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Log masuk ke rangkaian"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiada akses Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Ketik untuk mendapatkan pilihan"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Rangkaian mudah alih tiada akses Internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Rangkaian tiada akses Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Pelayan DNS peribadi tidak boleh diakses"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> mempunyai kesambungan terhad"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Ketik untuk menyambung juga"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Beralih kepada <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Beralih daripada <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> kepada <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"jenis rangkaian tidak diketahui"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"data mudah alih"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-my/strings.xml b/service/ServiceConnectivityResources/res/values-my/strings.xml
new file mode 100644
index 0000000..50590fa
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-my/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"ဝိုင်ဖိုင်ကွန်ရက်သို့ လက်မှတ်ထိုးဝင်ပါ"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"ကွန်ယက်သို့ လက်မှတ်ထိုးဝင်ရန်"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> တွင် အင်တာနက်အသုံးပြုခွင့် မရှိပါ"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"အခြားရွေးချယ်စရာများကိုကြည့်ရန် တို့ပါ"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"မိုဘိုင်းကွန်ရက်တွင် အင်တာနက်ချိတ်ဆက်မှု မရှိပါ"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"ကွန်ရက်တွင် အင်တာနက်အသုံးပြုခွင့် မရှိပါ"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"သီးသန့် ဒီအန်အက်စ် (DNS) ဆာဗာကို သုံး၍မရပါ။"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> တွင် ချိတ်ဆက်မှုကို ကန့်သတ်ထားသည်"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"မည်သို့ပင်ဖြစ်စေ ချိတ်ဆက်ရန် တို့ပါ"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> သို့ ပြောင်းလိုက်ပြီ"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> မှ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> သို့ ပြောင်းလိုက်ပြီ"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"အမည်မသိကွန်ရက်အမျိုးအစား"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"မိုဘိုင်းဒေတာ"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"ဘလူးတုသ်"</item>
+    <item msgid="1616528372438698248">"အီသာနက်"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-nb/strings.xml b/service/ServiceConnectivityResources/res/values-nb/strings.xml
new file mode 100644
index 0000000..b89d198
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-nb/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Logg på Wi-Fi-nettverket"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Logg på nettverk"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internettilkobling"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Trykk for å få alternativer"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobilnettverket har ingen internettilgang"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Nettverket har ingen internettilgang"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Den private DNS-tjeneren kan ikke nås"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har begrenset tilkobling"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Trykk for å koble til likevel"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Byttet til <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Byttet fra <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> til <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"en ukjent nettverkstype"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobildata"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ne/strings.xml b/service/ServiceConnectivityResources/res/values-ne/strings.xml
new file mode 100644
index 0000000..bdcfa3b
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ne/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Wi-Fi नेटवर्कमा साइन इन गर्नुहोस्"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"सञ्जालमा साइन इन गर्नुहोस्"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> को इन्टरनेटमाथि पहुँच छैन"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"विकल्पहरूका लागि ट्याप गर्नुहोस्"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"मोबाइल नेटवर्कको इन्टरनेटमाथि पहुँच छैन"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"नेटवर्कको इन्टरनेटमाथि पहुँच छैन"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"निजी DNS सर्भरमाथि पहुँच प्राप्त गर्न सकिँदैन"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> को जडान सीमित छ"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"जसरी भए पनि जडान गर्न ट्याप गर्नुहोस्"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> मा बदल्नुहोस्"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> बाट <xliff:g id="NEW_NETWORK">%2$s</xliff:g> मा परिवर्तन गरियो"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"नेटवर्कको कुनै अज्ञात प्रकार"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"मोबाइल डेटा"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"ब्लुटुथ"</item>
+    <item msgid="1616528372438698248">"इथरनेट"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-nl/strings.xml b/service/ServiceConnectivityResources/res/values-nl/strings.xml
new file mode 100644
index 0000000..8ecff6e
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-nl/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Inloggen bij wifi-netwerk"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Inloggen bij netwerk"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> heeft geen internettoegang"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Tik voor opties"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobiel netwerk heeft geen internettoegang"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Netwerk heeft geen internettoegang"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Geen toegang tot privé-DNS-server"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> heeft beperkte connectiviteit"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Tik om toch verbinding te maken"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Overgeschakeld naar <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Overgeschakeld van <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> naar <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"een onbekend netwerktype"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobiele data"</item>
+    <item msgid="5520925862115353992">"Wifi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-or/strings.xml b/service/ServiceConnectivityResources/res/values-or/strings.xml
new file mode 100644
index 0000000..6ec1f9d
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-or/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"ୱାଇ-ଫାଇ ନେଟୱର୍କରେ ସାଇନ୍‍-ଇନ୍‍ କରନ୍ତୁ"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"ନେଟ୍‌ୱର୍କରେ ସାଇନ୍‍ ଇନ୍‍ କରନ୍ତୁ"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ର ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"ବିକଳ୍ପ ପାଇଁ ଟାପ୍‍ କରନ୍ତୁ"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"ମୋବାଇଲ୍ ନେଟ୍‌ୱାର୍କରେ ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"ନେଟ୍‌ୱାର୍କରେ ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"ବ୍ୟକ୍ତିଗତ DNS ସର୍ଭର୍ ଆକ୍ସେସ୍ କରିହେବ ନାହିଁ"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ର ସୀମିତ ସଂଯୋଗ ଅଛି"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"ତଥାପି ଯୋଗାଯୋଗ କରିବାକୁ ଟାପ୍ କରନ୍ତୁ"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>କୁ ବଦଳାଗଲା"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ରୁ <xliff:g id="NEW_NETWORK">%2$s</xliff:g>କୁ ବଦଳାଗଲା"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"ଏକ ଅଜଣା ନେଟ୍‌ୱର୍କ ପ୍ରକାର"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"ମୋବାଇଲ୍‌ ଡାଟା"</item>
+    <item msgid="5520925862115353992">"ୱାଇ-ଫାଇ"</item>
+    <item msgid="1055487873974272842">"ବ୍ଲୁଟୁଥ"</item>
+    <item msgid="1616528372438698248">"ଇଥରନେଟ୍‌"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-pa/strings.xml b/service/ServiceConnectivityResources/res/values-pa/strings.xml
new file mode 100644
index 0000000..e948193
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-pa/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"ਵਾਈ-ਫਾਈ ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"ਵਿਕਲਪਾਂ ਲਈ ਟੈਪ ਕਰੋ"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"ਮੋਬਾਈਲ ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਹੈ"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"ਨਿੱਜੀ ਡੋਮੇਨ ਨਾਮ ਪ੍ਰਣਾਲੀ (DNS) ਸਰਵਰ \'ਤੇ ਪਹੁੰਚ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕੀ"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ਕੋਲ ਸੀਮਤ ਕਨੈਕਟੀਵਿਟੀ ਹੈ"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"ਫਿਰ ਵੀ ਕਨੈਕਟ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"ਬਦਲਕੇ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ਲਿਆਂਦਾ ਗਿਆ"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ਤੋਂ ਬਦਲਕੇ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> \'ਤੇ ਕੀਤਾ ਗਿਆ"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"ਇੱਕ ਅਗਿਆਤ ਨੈੱਟਵਰਕ ਕਿਸਮ"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"ਮੋਬਾਈਲ ਡਾਟਾ"</item>
+    <item msgid="5520925862115353992">"ਵਾਈ-ਫਾਈ"</item>
+    <item msgid="1055487873974272842">"ਬਲੂਟੁੱਥ"</item>
+    <item msgid="1616528372438698248">"ਈਥਰਨੈੱਟ"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-pl/strings.xml b/service/ServiceConnectivityResources/res/values-pl/strings.xml
new file mode 100644
index 0000000..038328f
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-pl/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Zaloguj się w sieci Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Zaloguj się do sieci"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nie ma dostępu do internetu"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Kliknij, by wyświetlić opcje"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Sieć komórkowa nie ma dostępu do internetu"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Sieć nie ma dostępu do internetu"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Brak dostępu do prywatnego serwera DNS"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ma ograniczoną łączność"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Kliknij, by mimo to nawiązać połączenie"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Zmieniono na połączenie typu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"nieznany typ sieci"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobilna transmisja danych"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml b/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..fe37405
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Fazer login na rede Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Fazer login na rede"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Toque para ver opções"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"A rede móvel não tem acesso à Internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"A rede não tem acesso à Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Não é possível acessar o servidor DNS privado"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tem conectividade limitada"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Toque para conectar mesmo assim"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Alternado para <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Alternado de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> para <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"um tipo de rede desconhecido"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"dados móveis"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml b/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..69060f7
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Iniciar sessão na rede Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Início de sessão na rede"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Toque para obter mais opções"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"A rede móvel não tem acesso à Internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"A rede não tem acesso à Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Não é possível aceder ao servidor DNS."</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tem conetividade limitada."</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Toque para ligar mesmo assim."</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Mudou para <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Mudou de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> para <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"um tipo de rede desconhecido"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"dados móveis"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-pt/strings.xml b/service/ServiceConnectivityResources/res/values-pt/strings.xml
new file mode 100644
index 0000000..fe37405
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-pt/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Fazer login na rede Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Fazer login na rede"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Toque para ver opções"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"A rede móvel não tem acesso à Internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"A rede não tem acesso à Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Não é possível acessar o servidor DNS privado"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tem conectividade limitada"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Toque para conectar mesmo assim"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Alternado para <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Alternado de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> para <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"um tipo de rede desconhecido"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"dados móveis"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ro/strings.xml b/service/ServiceConnectivityResources/res/values-ro/strings.xml
new file mode 100644
index 0000000..227b7be
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ro/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Conectați-vă la rețeaua Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Conectați-vă la rețea"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nu are acces la internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Atingeți pentru opțiuni"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Rețeaua mobilă nu are acces la internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Rețeaua nu are acces la internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Serverul DNS privat nu poate fi accesat"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> are conectivitate limitată"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Atingeți pentru a vă conecta oricum"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"S-a comutat la <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"un tip de rețea necunoscut"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"date mobile"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ru/strings.xml b/service/ServiceConnectivityResources/res/values-ru/strings.xml
new file mode 100644
index 0000000..18acf81
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ru/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Подключение к Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Регистрация в сети"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"Сеть \"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>\" не подключена к Интернету"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Нажмите, чтобы показать варианты."</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Мобильная сеть не подключена к Интернету"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Сеть не подключена к Интернету"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Доступа к частному DNS-серверу нет."</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"Подключение к сети \"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>\" ограничено"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Нажмите, чтобы подключиться"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Новое подключение: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"Устройство использует <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="501662047275723743">"Устройство отключено от сети <xliff:g id="NEW_NETWORK">%2$s</xliff:g> и теперь использует <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"неизвестный тип сети"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"мобильный Интернет"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-si/strings.xml b/service/ServiceConnectivityResources/res/values-si/strings.xml
new file mode 100644
index 0000000..6307c2b
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-si/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Wi-Fi ජාලයට පුරනය වන්න"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"ජාලයට පුරනය වන්න"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> හට අන්තර්ජාල ප්‍රවේශය නැත"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"විකල්ප සඳහා තට්ටු කරන්න"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"ජංගම ජාලවලට අන්තර්ජාල ප්‍රවේශය නැත"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"ජාලයට අන්තර්ජාල ප්‍රවේශය නැත"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"පුද්ගලික DNS සේවාදායකයට ප්‍රවේශ වීමට නොහැකිය"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> හට සීමිත සබැඳුම් හැකියාවක් ඇත"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"කෙසේ වෙතත් ඉදිරියට යාමට තට්ටු කරන්න"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> වෙත මාරු විය"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"උපාංගය <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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> සිට <xliff:g id="NEW_NETWORK">%2$s</xliff:g> වෙත මාරු විය"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"නොදන්නා ජාල වර්ගයකි"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"ජංගම දත්ත"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"බ්ලූටූත්"</item>
+    <item msgid="1616528372438698248">"ඊතර්නෙට්"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-sk/strings.xml b/service/ServiceConnectivityResources/res/values-sk/strings.xml
new file mode 100644
index 0000000..e894fef
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-sk/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Prihlásiť sa do siete Wi‑Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Prihlásenie do siete"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nemá prístup k internetu"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Klepnutím získate možnosti"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobilná sieť nemá prístup k internetu"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Sieť nemá prístup k internetu"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"K súkromnému serveru DNS sa nepodarilo získať prístup"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> má obmedzené pripojenie"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Ak sa chcete aj napriek tomu pripojiť, klepnite"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Prepnuté na sieť: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"neznámy typ siete"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobilné dáta"</item>
+    <item msgid="5520925862115353992">"Wi‑Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-sl/strings.xml b/service/ServiceConnectivityResources/res/values-sl/strings.xml
new file mode 100644
index 0000000..954b324
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-sl/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Prijavite se v omrežje Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Prijava v omrežje"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"Omrežje <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nima dostopa do interneta"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Dotaknite se za možnosti"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobilno omrežje nima dostopa do interneta"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Omrežje nima dostopa do interneta"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Do zasebnega strežnika DNS ni mogoče dostopati"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"Povezljivost omrežja <xliff:g id="NETWORK_SSID">%1$s</xliff:g> je omejena"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Dotaknite se, da kljub temu vzpostavite povezavo"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Preklopljeno na omrežje vrste <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"neznana vrsta omrežja"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"prenos podatkov v mobilnem omrežju"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-sq/strings.xml b/service/ServiceConnectivityResources/res/values-sq/strings.xml
new file mode 100644
index 0000000..bd5d052
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-sq/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Identifikohu në rrjetin Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Identifikohu në rrjet"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nuk ka qasje në internet"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Trokit për opsionet"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Rrjeti celular nuk ka qasje në internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Rrjeti nuk ka qasje në internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Serveri privat DNS nuk mund të qaset"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ka lidhshmëri të kufizuar"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Trokit për t\'u lidhur gjithsesi"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Kaloi te <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Kaloi nga <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> te <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"një lloj rrjeti i panjohur"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"të dhënat celulare"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Eternet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-sr/strings.xml b/service/ServiceConnectivityResources/res/values-sr/strings.xml
new file mode 100644
index 0000000..8bedbff
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-sr/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Пријављивање на WiFi мрежу"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Пријавите се на мрежу"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> нема приступ интернету"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Додирните за опције"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Мобилна мрежа нема приступ интернету"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Мрежа нема приступ интернету"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Приступ приватном DNS серверу није успео"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> има ограничену везу"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Додирните да бисте се ипак повезали"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Прешли сте на тип мреже <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"Уређај користи тип мреже <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="501662047275723743">"Прешли сте са типа мреже <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> на тип мреже <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"непознат тип мреже"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"мобилни подаци"</item>
+    <item msgid="5520925862115353992">"WiFi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Етернет"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-sv/strings.xml b/service/ServiceConnectivityResources/res/values-sv/strings.xml
new file mode 100644
index 0000000..b3f1763
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-sv/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Logga in på ett Wi-Fi-nätverk"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Logga in på nätverket"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internetanslutning"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Tryck för alternativ"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobilnätverket har ingen internetanslutning"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Nätverket har ingen internetanslutning"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Det går inte att komma åt den privata DNS-servern."</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har begränsad anslutning"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Tryck för att ansluta ändå"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Byte av nätverk till <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"en okänd nätverkstyp"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobildata"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-sw/strings.xml b/service/ServiceConnectivityResources/res/values-sw/strings.xml
new file mode 100644
index 0000000..9674654
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-sw/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Ingia kwa mtandao wa Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Ingia katika mtandao"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> haina uwezo wa kufikia intaneti"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Gusa ili upate chaguo"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mtandao wa simu hauna uwezo wa kufikia intaneti"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Mtandao hauna uwezo wa kufikia intaneti"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Seva ya faragha ya DNS haiwezi kufikiwa"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ina muunganisho unaofikia huduma chache."</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Gusa ili uunganishe tu"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Sasa inatumia <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"aina ya mtandao isiyojulikana"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"data ya simu"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethaneti"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ta/strings.xml b/service/ServiceConnectivityResources/res/values-ta/strings.xml
new file mode 100644
index 0000000..12604cb
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ta/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"வைஃபை நெட்வொர்க்கில் உள்நுழையவும்"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"நெட்வொர்க்கில் உள்நுழையவும்"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"விருப்பங்களுக்கு, தட்டவும்"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"மொபைல் நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"தனிப்பட்ட DNS சேவையகத்தை அணுக இயலாது"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> வரம்பிற்கு உட்பட்ட இணைப்புநிலையைக் கொண்டுள்ளது"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"எப்படியேனும் இணைப்பதற்குத் தட்டவும்"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>க்கு மாற்றப்பட்டது"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> இலிருந்து <xliff:g id="NEW_NETWORK">%2$s</xliff:g>க்கு மாற்றப்பட்டது"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"தெரியாத நெட்வொர்க் வகை"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"மொபைல் டேட்டா"</item>
+    <item msgid="5520925862115353992">"வைஃபை"</item>
+    <item msgid="1055487873974272842">"புளூடூத்"</item>
+    <item msgid="1616528372438698248">"ஈத்தர்நெட்"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-te/strings.xml b/service/ServiceConnectivityResources/res/values-te/strings.xml
new file mode 100644
index 0000000..84a8640
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-te/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Wi-Fi నెట్‌వర్క్‌కి సైన్ ఇన్ చేయండి"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"నెట్‌వర్క్‌కి సైన్ ఇన్ చేయండి"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>కి ఇంటర్నెట్ యాక్సెస్ లేదు"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"ఎంపికల కోసం నొక్కండి"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"మొబైల్ నెట్‌వర్క్‌కు ఇంటర్నెట్ యాక్సెస్ లేదు"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"నెట్‌వర్క్‌కు ఇంటర్నెట్ యాక్సెస్ లేదు"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"ప్రైవేట్ DNS సర్వర్‌ను యాక్సెస్ చేయడం సాధ్యపడదు"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> పరిమిత కనెక్టివిటీని కలిగి ఉంది"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"ఏదేమైనా కనెక్ట్ చేయడానికి నొక్కండి"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>కి మార్చబడింది"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"పరికరం <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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> నుండి <xliff:g id="NEW_NETWORK">%2$s</xliff:g>కి మార్చబడింది"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"తెలియని నెట్‌వర్క్ రకం"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"మొబైల్ డేటా"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"బ్లూటూత్"</item>
+    <item msgid="1616528372438698248">"ఈథర్‌నెట్"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-th/strings.xml b/service/ServiceConnectivityResources/res/values-th/strings.xml
new file mode 100644
index 0000000..1616e51
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-th/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"ลงชื่อเข้าใช้เครือข่าย WiFi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"ลงชื่อเข้าใช้เครือข่าย"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> เข้าถึงอินเทอร์เน็ตไม่ได้"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"แตะเพื่อดูตัวเลือก"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"เครือข่ายมือถือไม่มีการเข้าถึงอินเทอร์เน็ต"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"เครือข่ายไม่มีการเข้าถึงอินเทอร์เน็ต"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"เข้าถึงเซิร์ฟเวอร์ DNS ไม่ได้"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> มีการเชื่อมต่อจำกัด"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"แตะเพื่อเชื่อมต่อ"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"เปลี่ยนเป็น <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"อุปกรณ์จะใช้ <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="501662047275723743">"เปลี่ยนจาก <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> เป็น <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"ประเภทเครือข่ายที่ไม่รู้จัก"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"เน็ตมือถือ"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"บลูทูธ"</item>
+    <item msgid="1616528372438698248">"อีเทอร์เน็ต"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-tl/strings.xml b/service/ServiceConnectivityResources/res/values-tl/strings.xml
new file mode 100644
index 0000000..3bf1ce4
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-tl/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Mag-sign in sa Wi-Fi network"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Mag-sign in sa network"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"Walang access sa internet ang <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"I-tap para sa mga opsyon"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Walang access sa internet ang mobile network"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Walang access sa internet ang network"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Hindi ma-access ang pribadong DNS server"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"Limitado ang koneksyon ng <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"I-tap para kumonekta pa rin"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Lumipat sa <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"isang hindi kilalang uri ng network"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobile data"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-tr/strings.xml b/service/ServiceConnectivityResources/res/values-tr/strings.xml
new file mode 100644
index 0000000..5c326e5
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-tr/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Kablosuz ağda oturum açın"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Ağda oturum açın"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<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="634938444133558942">"Seçenekler için dokunun"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobil ağın internet bağlantısı yok"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Ağın internet bağlantısı yok"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Gizli DNS sunucusuna erişilemiyor"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> sınırlı bağlantıya sahip"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Yine de bağlanmak için dokunun"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ağına geçildi"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"<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="501662047275723743">"<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="network_switch_type_name_unknown" msgid="3665696841646851068">"bilinmeyen ağ türü"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobil veri"</item>
+    <item msgid="5520925862115353992">"Kablosuz"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-uk/strings.xml b/service/ServiceConnectivityResources/res/values-uk/strings.xml
new file mode 100644
index 0000000..d1382da
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-uk/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Вхід у мережу Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Вхід у мережу"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"Мережа <xliff:g id="NETWORK_SSID">%1$s</xliff:g> не має доступу до Інтернету"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Торкніться, щоб відкрити опції"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Мобільна мережа не має доступу до Інтернету"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Мережа не має доступу до Інтернету"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Немає доступу до приватного DNS-сервера"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"Підключення до мережі <xliff:g id="NETWORK_SSID">%1$s</xliff:g> обмежено"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Натисніть, щоб усе одно підключитися"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Пристрій перейшов на мережу <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"Коли мережа <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="501662047275723743">"Пристрій перейшов з мережі <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> на мережу <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"невідомий тип мережі"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"мобільне передавання даних"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"Мережа VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-ur/strings.xml b/service/ServiceConnectivityResources/res/values-ur/strings.xml
new file mode 100644
index 0000000..3c031ad
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-ur/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"‏Wi-Fi نیٹ ورک میں سائن ان کریں"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"نیٹ ورک میں سائن ان کریں"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> کو انٹرنیٹ تک رسائی حاصل نہیں ہے"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"اختیارات کیلئے تھپتھپائیں"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"موبائل نیٹ ورک کو انٹرنیٹ تک رسائی حاصل نہیں ہے"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"نیٹ ورک کو انٹرنیٹ تک رسائی حاصل نہیں ہے"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"‏نجی DNS سرور تک رسائی حاصل نہیں کی جا سکی"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> کی کنیکٹوٹی محدود ہے"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"بہر حال منسلک کرنے کے لیے تھپتھپائیں"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> پر سوئچ ہو گیا"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"جب <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="501662047275723743">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> سے <xliff:g id="NEW_NETWORK">%2$s</xliff:g> پر سوئچ ہو گیا"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"نیٹ ورک کی نامعلوم قسم"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"موبائل ڈیٹا"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"بلوٹوتھ"</item>
+    <item msgid="1616528372438698248">"ایتھرنیٹ"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-uz/strings.xml b/service/ServiceConnectivityResources/res/values-uz/strings.xml
new file mode 100644
index 0000000..7518db3
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-uz/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Wi-Fi tarmoqqa kirish"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Tarmoqqa kirish"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nomli tarmoqda internetga ruxsati yoʻq"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Variantlarni ko‘rsatish uchun bosing"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mobil tarmoq internetga ulanmagan"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Tarmoq internetga ulanmagan"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Xususiy DNS server ishlamayapti"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nomli tarmoqda aloqa cheklangan"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Baribir ulash uchun bosing"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Yangi ulanish: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"<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="network_switch_type_name_unknown" msgid="3665696841646851068">"noma’lum tarmoq turi"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"mobil internet"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-vi/strings.xml b/service/ServiceConnectivityResources/res/values-vi/strings.xml
new file mode 100644
index 0000000..d2284d4
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-vi/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Đăng nhập vào mạng Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Đăng nhập vào mạng"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<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="634938444133558942">"Nhấn để biết tùy chọn"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Mạng di động không có quyền truy cập Internet"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Mạng không có quyền truy cập Internet"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Không thể truy cập máy chủ DNS riêng tư"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<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="5741329444564575840">"Nhấn để tiếp tục kết nối"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Đã chuyển sang <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"Đã 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="network_switch_type_name_unknown" msgid="3665696841646851068">"loại mạng không xác định"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"dữ liệu di động"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"Bluetooth"</item>
+    <item msgid="1616528372438698248">"Ethernet"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..813482b
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"登录到WLAN网络"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"登录到网络"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 无法访问互联网"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"点按即可查看相关选项"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"此移动网络无法访问互联网"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"此网络无法访问互联网"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"无法访问私人 DNS 服务器"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 的连接受限"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"点按即可继续连接"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"已切换至<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"设备会在<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="501662047275723743">"已从<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>切换至<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"未知网络类型"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"移动数据"</item>
+    <item msgid="5520925862115353992">"WLAN"</item>
+    <item msgid="1055487873974272842">"蓝牙"</item>
+    <item msgid="1616528372438698248">"以太网"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..676404f
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"登入 Wi-Fi 網絡"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"登入網絡"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>未有連接至互聯網"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"輕按即可查看選項"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"流動網絡並未連接互聯網"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"網絡並未連接互聯網"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"無法存取私人 DNS 伺服器"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>連線受限"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"仍要輕按以連結至此網絡"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"已切換至<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"裝置會在 <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="501662047275723743">"已從<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>切換至<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"不明網絡類型"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"流動數據"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"藍牙"</item>
+    <item msgid="1616528372438698248">"以太網"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..f355138
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"登入 Wi-Fi 網路"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"登入網路"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 沒有網際網路連線"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"輕觸即可查看選項"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"這個行動網路沒有網際網路連線"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"這個網路沒有網際網路連線"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"無法存取私人 DNS 伺服器"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 的連線能力受限"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"輕觸即可繼續連線"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"已切換至<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"裝置會在無法連上「<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="501662047275723743">"已從 <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> 切換至<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string>
+    <string name="network_switch_type_name_unknown" msgid="3665696841646851068">"不明的網路類型"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"行動數據"</item>
+    <item msgid="5520925862115353992">"Wi-Fi"</item>
+    <item msgid="1055487873974272842">"藍牙"</item>
+    <item msgid="1616528372438698248">"乙太網路"</item>
+    <item msgid="9177085807664964627">"VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values-zu/strings.xml b/service/ServiceConnectivityResources/res/values-zu/strings.xml
new file mode 100644
index 0000000..55fefb7
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values-zu/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="wifi_available_sign_in" msgid="381054692557675237">"Ngena ngemvume kunethiwekhi ye-Wi-Fi"</string>
+    <string name="network_available_sign_in" msgid="1520342291829283114">"Ngena ngemvume kunethiwekhi"</string>
+    <!-- no translation found for network_available_sign_in_detailed (7520423801613396556) -->
+    <string name="wifi_no_internet" msgid="1386911698276448061">"I-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ayinakho ukufinyelela kwe-inthanethi"</string>
+    <string name="wifi_no_internet_detailed" msgid="634938444133558942">"Thepha ukuze uthole izinketho"</string>
+    <string name="mobile_no_internet" msgid="4014455157529909781">"Inethiwekhi yeselula ayinakho ukufinyelela kwe-inthanethi"</string>
+    <string name="other_networks_no_internet" msgid="6698711684200067033">"Inethiwekhi ayinakho ukufinyelela kwenethiwekhi"</string>
+    <string name="private_dns_broken_detailed" msgid="3709388271074611847">"Iseva eyimfihlo ye-DNS ayikwazi ukufinyelelwa"</string>
+    <string name="network_partial_connectivity" msgid="4791024923851432291">"I-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> inokuxhumeka okukhawulelwe"</string>
+    <string name="network_partial_connectivity_detailed" msgid="5741329444564575840">"Thepha ukuze uxhume noma kunjalo"</string>
+    <string name="network_switch_metered" msgid="1531869544142283384">"Kushintshelwe ku-<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string>
+    <string name="network_switch_metered_detail" msgid="1358296010128405906">"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="501662047275723743">"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="network_switch_type_name_unknown" msgid="3665696841646851068">"uhlobo olungaziwa lwenethiwekhi"</string>
+  <string-array name="network_switch_type_name">
+    <item msgid="2255670471736226365">"idatha yeselula"</item>
+    <item msgid="5520925862115353992">"I-Wi-Fi"</item>
+    <item msgid="1055487873974272842">"I-Bluetooth"</item>
+    <item msgid="1616528372438698248">"I-Ethernet"</item>
+    <item msgid="9177085807664964627">"I-VPN"</item>
+  </string-array>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
new file mode 100644
index 0000000..078a9eb
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- Configuration values for ConnectivityService
+     DO NOT EDIT THIS FILE for specific device configuration; instead, use a Runtime Resources
+     Overlay package following the overlayable.xml configuration in the same directory:
+     https://source.android.com/devices/architecture/rros -->
+<resources>
+
+    <!-- Configuration hook for the URL returned by ConnectivityManager#getCaptivePortalServerUrl.
+         If empty, the returned value is controlled by Settings.Global.CAPTIVE_PORTAL_HTTP_URL,
+         and if that value is empty, the framework will use a hard-coded default.
+         This is *NOT* a URL that will always be used by the system network validation to detect
+         captive portals: NetworkMonitor may use different strategies and will not necessarily use
+         this URL. NetworkMonitor behaviour should be configured with NetworkStack resource overlays
+         instead. -->
+    <!--suppress CheckTagEmptyBody -->
+    <string translatable="false" name="config_networkCaptivePortalServerUrl"></string>
+
+    <!-- The maximum duration (in milliseconds) we expect a network transition to take -->
+    <integer name="config_networkTransitionTimeout">60000</integer>
+
+    <!-- Configuration of network interfaces that support WakeOnLAN -->
+    <string-array translatable="false" name="config_wakeonlan_supported_interfaces">
+        <!--
+        <item>wlan0</item>
+        <item>eth0</item>
+        -->
+    </string-array>
+
+    <string-array translatable="false" name="config_legacy_networktype_restore_timers">
+        <item>2,60000</item><!-- mobile_mms -->
+        <item>3,60000</item><!-- mobile_supl -->
+        <item>4,60000</item><!-- mobile_dun -->
+        <item>5,60000</item><!-- mobile_hipri -->
+        <item>10,60000</item><!-- mobile_fota -->
+        <item>11,60000</item><!-- mobile_ims -->
+        <item>12,60000</item><!-- mobile_cbs -->
+    </string-array>
+
+    <!-- Default supported concurrent socket keepalive slots per transport type, used by
+         ConnectivityManager.createSocketKeepalive() for calculating the number of keepalive
+         offload slots that should be reserved for privileged access. This string array should be
+         overridden by the device to present the capability of creating socket keepalives. -->
+    <!-- An Array of "[NetworkCapabilities.TRANSPORT_*],[supported keepalives] -->
+    <string-array translatable="false" name="config_networkSupportedKeepaliveCount">
+        <item>0,1</item>
+        <item>1,3</item>
+    </string-array>
+
+    <!-- Reserved privileged keepalive slots per transport. -->
+    <integer translatable="false" name="config_reservedPrivilegedKeepaliveSlots">2</integer>
+
+    <!-- Allowed unprivileged keepalive slots per uid. -->
+    <integer translatable="false" name="config_allowedUnprivilegedKeepalivePerUid">2</integer>
+
+    <!-- Default value for ConnectivityManager.getMultipathPreference() on metered networks. Actual
+         device behaviour is controlled by the metered multipath preference in
+         ConnectivitySettingsManager. This is the default value of that setting. -->
+    <integer translatable="false" name="config_networkMeteredMultipathPreference">0</integer>
+
+    <!-- Whether the device should automatically switch away from Wi-Fi networks that lose
+         Internet access. Actual device behaviour is controlled by
+         Settings.Global.NETWORK_AVOID_BAD_WIFI. This is the default value of that setting. -->
+    <integer translatable="false" name="config_networkAvoidBadWifi">1</integer>
+
+    <!-- Array of ConnectivityManager.TYPE_xxxx constants for networks that may only
+         be controlled by systemOrSignature apps.  -->
+    <integer-array translatable="false" name="config_protectedNetworks">
+        <item>10</item>
+        <item>11</item>
+        <item>12</item>
+        <item>14</item>
+        <item>15</item>
+    </integer-array>
+
+    <!-- Whether the internal vehicle network should remain active even when no
+         apps requested it. -->
+    <bool name="config_vehicleInternalNetworkAlwaysRequested">false</bool>
+
+
+    <!-- If the hardware supports specially marking packets that caused a wakeup of the
+         main CPU, set this value to the mark used. -->
+    <integer name="config_networkWakeupPacketMark">0</integer>
+
+    <!-- Mask to use when checking skb mark defined in config_networkWakeupPacketMark above. -->
+    <integer name="config_networkWakeupPacketMask">0</integer>
+
+    <!-- Whether/how to notify the user on network switches. See LingerMonitor.java. -->
+    <integer translatable="false" name="config_networkNotifySwitchType">0</integer>
+
+    <!-- What types of network switches to notify. See LingerMonitor.java. -->
+    <string-array translatable="false" name="config_networkNotifySwitches">
+    </string-array>
+
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
new file mode 100644
index 0000000..f0f4ae8
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <overlayable name="ServiceConnectivityResourcesConfig">
+        <policy type="product|system|vendor">
+            <!-- Configuration values for ConnectivityService -->
+            <item type="array" name="config_legacy_networktype_restore_timers"/>
+            <item type="string" name="config_networkCaptivePortalServerUrl"/>
+            <item type="integer" name="config_networkTransitionTimeout"/>
+            <item type="array" name="config_wakeonlan_supported_interfaces"/>
+            <item type="integer" name="config_networkMeteredMultipathPreference"/>
+            <item type="array" name="config_networkSupportedKeepaliveCount"/>
+            <item type="integer" name="config_networkAvoidBadWifi"/>
+            <item type="array" name="config_protectedNetworks"/>
+            <item type="bool" name="config_vehicleInternalNetworkAlwaysRequested"/>
+            <item type="integer" name="config_networkWakeupPacketMark"/>
+            <item type="integer" name="config_networkWakeupPacketMask"/>
+            <item type="integer" name="config_networkNotifySwitchType"/>
+            <item type="array" name="config_networkNotifySwitches"/>
+
+        </policy>
+    </overlayable>
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values/strings.xml b/service/ServiceConnectivityResources/res/values/strings.xml
new file mode 100644
index 0000000..b2fa5f5
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values/strings.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- The System Connectivity Resources package is an internal system package that provides
+         configuration values for system networking that were pre-configured in the device. This
+         is the name of the package to display in the list of system apps. [CHAR LIMIT=40] -->
+    <string name="connectivityResourcesAppLabel">System Connectivity Resources</string>
+
+    <!-- A notification is shown when a wifi captive portal network is detected.  This is the notification's title. -->
+    <string name="wifi_available_sign_in">Sign in to Wi-Fi network</string>
+
+    <!-- A notification is shown when a captive portal network is detected.  This is the notification's title. -->
+    <string name="network_available_sign_in">Sign in to network</string>
+
+    <!-- A notification is shown when a captive portal network is detected.  This is the notification's message. -->
+    <string name="network_available_sign_in_detailed"><xliff:g id="network_ssid">%1$s</xliff:g></string>
+
+    <!-- A notification is shown when the user connects to a Wi-Fi network and the system detects that that network has no Internet access. This is the notification's title. -->
+    <string name="wifi_no_internet"><xliff:g id="network_ssid" example="GoogleGuest">%1$s</xliff:g> has no internet access</string>
+
+    <!-- A notification is shown when the user connects to a Wi-Fi network and the system detects that that network has no Internet access. This is the notification's message. -->
+    <string name="wifi_no_internet_detailed">Tap for options</string>
+
+    <!-- A notification is shown when the user connects to a mobile network without internet access. This is the notification's title. -->
+    <string name="mobile_no_internet">Mobile network has no internet access</string>
+
+    <!-- A notification is shown when the user connects to a non-mobile and non-wifi network without internet access. This is the notification's title. -->
+    <string name="other_networks_no_internet">Network has no internet access</string>
+
+    <!-- A notification is shown when connected network without internet due to private dns validation failed. This is the notification's message. [CHAR LIMIT=NONE] -->
+    <string name="private_dns_broken_detailed">Private DNS server cannot be accessed</string>
+
+    <!-- A notification is shown when the user connects to a network that doesn't have access to some services (e.g. Push notifications may not work). This is the notification's title. [CHAR LIMIT=50] -->
+    <string name="network_partial_connectivity"><xliff:g id="network_ssid" example="GoogleGuest">%1$s</xliff:g> has limited connectivity</string>
+
+    <!-- A notification is shown when the user connects to a network that doesn't have access to some services (e.g. Push notifications may not work). This is the notification's message. [CHAR LIMIT=50] -->
+    <string name="network_partial_connectivity_detailed">Tap to connect anyway</string>
+
+    <!-- A notification might be shown if the device switches to another network type (e.g., mobile data) because it detects that the network it was using (e.g., Wi-Fi) has lost Internet connectivity. This is the notification's title. %1$s is the network type that the device switched to, e.g., cellular data. It is one of the strings in the network_switch_type_name array. -->
+    <string name="network_switch_metered">Switched to <xliff:g id="network_type">%1$s</xliff:g></string>
+
+    <!-- A notification might be shown if the device switches to another network type (e.g., mobile data) because it detects that the network it was using (e.g., Wi-Fi) has lost Internet connectivity. This is the notification's message. %1$s is the network that the device switched to, e.g., cellular data. %2$s is the network type the device switched from, e.g., Wi-Fi. Both are strings in the network_switch_type_name array. -->
+    <string name="network_switch_metered_detail">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>
+
+    <!-- A toast might be shown if the device switches to another network type (e.g., mobile data) because it detects that the network it was using (e.g., Wi-Fi) has lost Internet connectivity. This is the text of the toast. %1$s is the network that the device switched from, e.g., Wi-Fi. %2$s is the network type the device switched from, e.g., cellular data. Both are strings in the network_switch_type_name array. -->
+    <string name="network_switch_metered_toast">Switched from <xliff:g id="previous_network">%1$s</xliff:g> to <xliff:g id="new_network">%2$s</xliff:g></string>
+
+    <!-- Network type names used in the network_switch_metered and network_switch_metered_detail strings. These must be kept in the sync with the values NetworkCapabilities.TRANSPORT_xxx values, and in the same order. -->
+    <string-array name="network_switch_type_name">
+        <item>mobile data</item>
+        <item>Wi-Fi</item>
+        <item>Bluetooth</item>
+        <item>Ethernet</item>
+        <item>VPN</item>
+    </string-array>
+
+    <!-- Network type name displayed if one of the types is not found in network_switch_type_name. -->
+    <string name="network_switch_type_name_unknown">an unknown network type</string>
+
+</resources>
diff --git a/service/ServiceConnectivityResources/resources-certs/com.android.connectivity.resources.pk8 b/service/ServiceConnectivityResources/resources-certs/com.android.connectivity.resources.pk8
new file mode 100644
index 0000000..bfdc28b
--- /dev/null
+++ b/service/ServiceConnectivityResources/resources-certs/com.android.connectivity.resources.pk8
Binary files differ
diff --git a/service/ServiceConnectivityResources/resources-certs/com.android.connectivity.resources.x509.pem b/service/ServiceConnectivityResources/resources-certs/com.android.connectivity.resources.x509.pem
new file mode 100644
index 0000000..70eca1c
--- /dev/null
+++ b/service/ServiceConnectivityResources/resources-certs/com.android.connectivity.resources.x509.pem
@@ -0,0 +1,36 @@
+-----BEGIN CERTIFICATE-----
+MIIGQzCCBCugAwIBAgIUZY8nxBMINp/79sziXU77MLPpEXowDQYJKoZIhvcNAQEL
+BQAwga8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy
+b2lkMSswKQYDVQQDDCJjb20uYW5kcm9pZC5jb25uZWN0aXZpdHkucmVzb3VyY2Vz
+MSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29tMCAXDTIxMDQyMjA3
+MjkxMFoYDzQ3NTkwMzE5MDcyOTEwWjCBrzELMAkGA1UEBhMCVVMxEzARBgNVBAgM
+CkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEDAOBgNVBAoMB0Fu
+ZHJvaWQxEDAOBgNVBAsMB0FuZHJvaWQxKzApBgNVBAMMImNvbS5hbmRyb2lkLmNv
+bm5lY3Rpdml0eS5yZXNvdXJjZXMxIjAgBgkqhkiG9w0BCQEWE2FuZHJvaWRAYW5k
+cm9pZC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC361NT9qSz
+h3uLcLBD67HNE1QX3ykwGyw8u7ExzqpsqLCzZsOCFRJQJY+CnrgNaAz0NXeNtx7D
+Lpr9OCWWbG1KTQ/ANlR8g6xCqlAk4xdixsAnIlBUJB90+RlkcWrliEY7OwcqIu3x
+/qe+5UR3irIFZOApNHOm760PjRl7VWAnYZC/PhkW0iKwnBuE96ddPIJc+KuiqCcP
+KflgF4/jmbHTZ+5uvVV4qkfovc744HnQtQoCDoYR8WpsJv3YL5xrAv78o3WCRzx6
+xxB+eUlJpuyyfIee2lUCG4Ly4jgOsWaupnUglLDORnz/L8fhhnpv83wLal7E0Shx
+sqvzZZbb1QLuwMWy++gfzdDvGWewES3BdSFp5NwYWXQGZWSkEEFbIiorKSurU1On
+9OwB0jT/H2B/CAFKYJQ2V+hQ4I7PG+z9p7ZFNR6GZbZuhEr+Dpq1CwtI3W45izr3
+RJgcc2IP6Oj7/XC2MmKGMqZkybBWcvazdyAMHzk9EZIBT2Oru3dnOl3uVUUPeZRs
+xRzqaA0MAlyj+GJ9uziEr3W1j+U1CFEnNWtlD/jqcTAwmaOsn1GhWyMAo1KOrJ/o
+LcJvwk5P/0XEyeli7/DSUpGjYiAgWMHWCOn9s6aYw3YFb+A/SgX3/+FIDib/vHTX
+i76JZfO0CfoKsbFDCH9KOMupHM9EO3ftQwIDAQABo1MwUTAdBgNVHQ4EFgQU/KGg
+gmMqXD5YOe5+B0W+YezN9LcwHwYDVR0jBBgwFoAU/KGggmMqXD5YOe5+B0W+YezN
+9LcwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAhr+AaNaIlRyM
+WKyJ+2Aa35fH5e44Xr/xPpriM5HHxsj0evjMCODqCQ7kzfwSEmtXh5uZYYNKb/JP
+ZMDHIFcYi1QCvm6E6YOd+Qn9CVxrwaDhigJv7ylhVf8q201GTvHhJIU99yFIrzJQ
+RNhxw+pNo7FYMZr3J7JZPAy60DN1KZvRV4FjZx5qiPUMyu4zVygzDkr0v5Ilncdp
+l9VVjOi7ocHyBKI+7RkXl97xN4SUe3vszwZQHCVyVopBw+YrMbDBCrknrQzUEgie
+BuI+kj5oOeiQ0P1i1K+UCCAjrLwhNyc9H02rKUtBHxa2AVjw7YpAJlBesb49Qvq+
+5L6JjHFVSSOEbIjboNib26zNackjbiefF74meSUbGVGfcJ1OdkZsXZWphmER8V7X
+Wz3Z8JwOXW1RLPgcbjilHUR5g8pEmWBv4KrTCSg5IvOJr4w3pyyMBiiVI9NI5sB7
+g5Mi9v3ifPD1OHA4Y3wYCb26mMEpRb8ogOhMHcGNbdnL3QtIUg4cmXGqGSY/LbpU
+np0sIQDSjc46o79F0boPsLlaN3US5WZIu0nc9SHkjoNhd0CJQ5r9aEn4/wNrZgxs
+s8OEKsqcS7OsWiIE6nG51TMDsCuyRBrGedtSUyFFSVSpivpYIrPVNKKlHsJ/o+Nv
+Udb6dBjCraPvJB8binB1aojwya3MwRs=
+-----END CERTIFICATE-----
diff --git a/service/ServiceConnectivityResources/resources-certs/key.pem b/service/ServiceConnectivityResources/resources-certs/key.pem
new file mode 100644
index 0000000..38771c2
--- /dev/null
+++ b/service/ServiceConnectivityResources/resources-certs/key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC361NT9qSzh3uL
+cLBD67HNE1QX3ykwGyw8u7ExzqpsqLCzZsOCFRJQJY+CnrgNaAz0NXeNtx7DLpr9
+OCWWbG1KTQ/ANlR8g6xCqlAk4xdixsAnIlBUJB90+RlkcWrliEY7OwcqIu3x/qe+
+5UR3irIFZOApNHOm760PjRl7VWAnYZC/PhkW0iKwnBuE96ddPIJc+KuiqCcPKflg
+F4/jmbHTZ+5uvVV4qkfovc744HnQtQoCDoYR8WpsJv3YL5xrAv78o3WCRzx6xxB+
+eUlJpuyyfIee2lUCG4Ly4jgOsWaupnUglLDORnz/L8fhhnpv83wLal7E0Shxsqvz
+ZZbb1QLuwMWy++gfzdDvGWewES3BdSFp5NwYWXQGZWSkEEFbIiorKSurU1On9OwB
+0jT/H2B/CAFKYJQ2V+hQ4I7PG+z9p7ZFNR6GZbZuhEr+Dpq1CwtI3W45izr3RJgc
+c2IP6Oj7/XC2MmKGMqZkybBWcvazdyAMHzk9EZIBT2Oru3dnOl3uVUUPeZRsxRzq
+aA0MAlyj+GJ9uziEr3W1j+U1CFEnNWtlD/jqcTAwmaOsn1GhWyMAo1KOrJ/oLcJv
+wk5P/0XEyeli7/DSUpGjYiAgWMHWCOn9s6aYw3YFb+A/SgX3/+FIDib/vHTXi76J
+ZfO0CfoKsbFDCH9KOMupHM9EO3ftQwIDAQABAoICAQCXM/GKqtAXBIBOT/Ops0C2
+n3hYM9BRy1UgDRKNJyG3OSwkIY0ECbzHhUmpkkEwTGWx8675JB43Sr6DBUDpnPRw
+zE/xrvjgcQQSvqAq40PbohhhU/WEZzoxWYVFrXS7hcBve4TVYGgMtlZEO4qBWNYo
+Vxlu5r9Z89tsWI0ldzgYyD5O64eG2nVIit6Y/11p6pAmTQ4WKHYMIm7xUA2siTPH
+4L8F7cQx8pQxxLI+q5WaPuweasBQShA7IAc7T1EiLRFitCOsWlJfgf6Oa7oTwhcA
+Wh7JOyf+Fo4ejlqVwcTwOss6YOPGge7LgQWr5HoORbeqTuXgmy/L4Z85+EABNOs1
+5muHZvsuPXSmW6g1bCi8zvQcjFIX31yBVg8zkdG8WRezFxiVlN8UFAx4rwo03aBs
+rDyU4GCxoUBvF/M9534l1gKOyr0hlQ40nQ4kBabbm2wWOKCVzmLEtFmWX9RV0tjX
+pEtTCqgsGlsIypLy21+uow8SBojhkZ+xORCF2XivGu6SKtvwGvjpYXpXrI6DN4Lw
+kH5J5FwSu1SNY8tnIEJEmj8IMTp+Vw20kwNVTcwdC2nJDDiezJum4PqZRdWIuupm
+BWzXD3fvMXqHmT02sJTQ+FRAgiQLLWDzNAYMJUofzuIwycs4iO9MOPHjkHScvk4N
+FXLrzFBSbdw+wi1DdzzMuQKCAQEA5wx07O5bHBHybs6tpwuZ0TuJ3OIVXh/ocNVR
+gSOCSMirv+K4u3jToXwjfTXUc9lcn+DenZPpGmUWF0sZ83ytZm1eFVgGZpP6941C
+waSeb8zGsgbEyZIQTVILfgtyPDwdtgu0d1Ip+ppj9czXmnxMY/ruHOX1Do1UfZoA
+UA1ytHJSjFKU6saAhHrdk91soTVzc/E3uo7U4Ff0L8/3tT3DAEFYxDXUCH8W2IZZ
+6zVvlqnPH4elxsPYM6rtIwq52reOTLNxC+SFSamK/82zu09Kjj5sQ6HKlvKJFiL5
+bULWu4lenoDfEN0lng+QopJTgZq4/tgOLum43C/Zd0PGC9R6PwKCAQEAy8fvPqwM
+gPbNasni9qGVG+FfiFd/wEMlgKlVVqi+WzF6tCAnXCQXXg3A7FpLQrX8hVKdMznq
+wPgM5AXP4FOguBFNk65chZmPizBIUDPJ4TNHI8FcGgcxbKGvDdVHsUpa/h5rJlvV
+GLJTKV4KjcsTjl5tlRsJ48bSfpBNQHpSKlCswT6jjteiDY6Rln0GFKQIKDHqp3I6
+Zn1E4yfdiIz9VnMPfg1fbjBeR7s1zNzlrR8Dv9oK9tkzI5G1wSbdzksg2O1q2tvg
+WrZrTAA3Uw6sPUMft0vk5Jw6a6CLkrcfayv3xDHwvM/4P3HgP8j9WQ8at8ttHpfD
+oWyt3fZ3pBuj/QKCAQANqxH7tjoTlgg2f+mL+Ua3NwN32rQS5mZUznnM3vHlJmHq
+rxnolURHyFU9IgMYe2JcXuwsfESM+C/vXtUBL33+kje/oX53cQemv2eUlw18ZavX
+ekkH96kZOeJOKZUvdQr46wZZDLZJCfsh3mVe0T2fqIePlBcELl4yM/sSwUjo3d5+
+SKBgpy+RJseW6MF1Y/kZgcqfMbXsM6fRcEciJK41hKggq2KIwiPy2TfWj0mzqwYC
+wn6PHKTcoZ73tLm786Hqba8hWfp8mhgL+/pG+XDaq1yyP48BkQWFFrqUuSCE5aKA
+U/VeRQblq9wNkgR4pVOOV++23MK/2+DMimjb6Ez3AoIBABIXK7wKlgmU32ONjKKM
+capJ9asq6WJuE5Q6dCL/U/bQi64V9KiPY6ur2OailW/UrBhB30a+64I6AxrzESM/
+CVON5a8omXoayc13edP05QUjAjvAXKbK4K5eJCY8OuMYUL+if6ymFmLc4dkYSiOQ
+Vaob4+qKvfQEoIcv1EvXEBhFlTCKmQaDShWeBHqxmqqWbUr0M3qt/1U95bGsxlPr
+AEp+aG+uTDyB+ryvd/U53wHhcPnFJ5gGbC3KL7J3+tTngoD/gq7vOhmTfC8BDehH
+sy61GMmy6R0KaX1IgVuC+j0PaC14qYB5jfZD675930/asWqDmqpOmsVn2n+L888T
+zRkCggEBAIMuNhhfGGY6E4PLUcPM0LZA4tI/wTpeYEahunU1hWIYo/iZB9od2biz
+EeYY4BtkzCoE5ZWYXqTgiMxN4hJ4ufB+5umZ4BO0Gyx4p2/Ik2uv1BXu++GbM+TI
+eeFmaBh00dTtjccpeZEDgNkjAO7Rh9GV2ifl3uhqg0MnFXywPUX2Vm2bmwQXnfV9
+wY2TXgOmBN2epFBOArJwiA5IfV+bSqXCFCx8fgyOWpMNq9+zDRd6KCeHyge54ahm
+jMhCncp1OPDPaV+gnUdgWDGcywYg0KQvu5dLuCFfvucnsWoH2txsVZrXFha5XSM4
+/4Pif3Aj5E9dm1zkUtZJYQbII5SKQ94=
+-----END PRIVATE KEY-----
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
new file mode 100644
index 0000000..5caa11b
--- /dev/null
+++ b/service/jarjar-rules.txt
@@ -0,0 +1,17 @@
+rule android.sysprop.** com.android.connectivity.sysprop.@1
+rule com.android.net.module.util.** com.android.connectivity.net-utils.@1
+rule com.android.modules.utils.** com.android.connectivity.modules-utils.@1
+
+# internal util classes
+# Exclude AsyncChannel. TODO: remove AsyncChannel usage in ConnectivityService
+rule com.android.internal.util.AsyncChannel* @0
+# Exclude LocationPermissionChecker. This is going to be moved to libs/net
+rule com.android.internal.util.LocationPermissionChecker* @0
+rule android.util.LocalLog* com.android.connectivity.util.LocalLog@1
+# android.util.IndentingPrintWriter* should use a different package name from
+# the one in com.android.internal.util
+rule android.util.IndentingPrintWriter* android.connectivity.util.IndentingPrintWriter@1
+rule com.android.internal.util.** com.android.connectivity.util.@1
+
+rule com.android.internal.messages.** com.android.connectivity.messages.@1
+rule com.google.protobuf.** com.android.connectivity.protobuf.@1
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
new file mode 100644
index 0000000..e7a40e5
--- /dev/null
+++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+#define LOG_NDEBUG 0
+
+#define LOG_TAG "TestNetworkServiceJni"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/if.h>
+#include <linux/if_tun.h>
+#include <linux/ipv6_route.h>
+#include <linux/route.h>
+#include <netinet/in.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <log/log.h>
+
+#include "jni.h"
+#include <android-base/stringprintf.h>
+#include <android-base/unique_fd.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedUtfChars.h>
+
+namespace android {
+
+//------------------------------------------------------------------------------
+
+static void throwException(JNIEnv* env, int error, const char* action, const char* iface) {
+    const std::string& msg = "Error: " + std::string(action) + " " + std::string(iface) +  ": "
+                + std::string(strerror(error));
+    jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
+}
+
+static int createTunTapInterface(JNIEnv* env, bool isTun, const char* iface) {
+    base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
+    ifreq ifr{};
+
+    // Allocate interface.
+    ifr.ifr_flags = (isTun ? IFF_TUN : IFF_TAP) | IFF_NO_PI;
+    strlcpy(ifr.ifr_name, iface, IFNAMSIZ);
+    if (ioctl(tun.get(), TUNSETIFF, &ifr)) {
+        throwException(env, errno, "allocating", ifr.ifr_name);
+        return -1;
+    }
+
+    // Activate interface using an unconnected datagram socket.
+    base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
+    ifr.ifr_flags = IFF_UP;
+
+    if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) {
+        throwException(env, errno, "activating", ifr.ifr_name);
+        return -1;
+    }
+
+    return tun.release();
+}
+
+//------------------------------------------------------------------------------
+
+static jint create(JNIEnv* env, jobject /* thiz */, jboolean isTun, jstring jIface) {
+    ScopedUtfChars iface(env, jIface);
+    if (!iface.c_str()) {
+        jniThrowNullPointerException(env, "iface");
+        return -1;
+    }
+
+    int tun = createTunTapInterface(env, isTun, iface.c_str());
+
+    // Any exceptions will be thrown from the createTunTapInterface call
+    return tun;
+}
+
+//------------------------------------------------------------------------------
+
+static const JNINativeMethod gMethods[] = {
+    {"jniCreateTunTap", "(ZLjava/lang/String;)I", (void*)create},
+};
+
+int register_android_server_TestNetworkService(JNIEnv* env) {
+    return jniRegisterNativeMethods(env, "com/android/server/TestNetworkService", gMethods,
+                                    NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
new file mode 100644
index 0000000..0012879
--- /dev/null
+++ b/service/jni/onload.cpp
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+#include <nativehelper/JNIHelp.h>
+#include <log/log.h>
+
+namespace android {
+
+int register_android_server_TestNetworkService(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) {
+        ALOGE("GetEnv failed");
+        return JNI_ERR;
+    }
+
+    if (register_android_server_TestNetworkService(env) < 0) {
+        return JNI_ERR;
+    }
+
+    return JNI_VERSION_1_6;
+}
+
+};
diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml
new file mode 100644
index 0000000..df57c22
--- /dev/null
+++ b/service/lint-baseline.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.telephony.TelephonyManager#isDataCapable`"
+        errorLine1="            if (tm.isDataCapable()) {"
+        errorLine2="                   ~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="791"
+            column="20"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.content.Context#sendStickyBroadcast`"
+        errorLine1="                mUserAllContext.sendStickyBroadcast(intent, options);"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="2693"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.content.pm.PackageManager#getTargetSdkVersion`"
+        errorLine1="            final int callingVersion = pm.getTargetSdkVersion(callingPackageName);"
+        errorLine2="                                          ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+            line="5894"
+            column="43"/>
+    </issue>
+
+</issues>
diff --git a/service/proto/connectivityproto.proto b/service/proto/connectivityproto.proto
new file mode 100644
index 0000000..a992d7c
--- /dev/null
+++ b/service/proto/connectivityproto.proto
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+// Connectivity protos can be created in this directory. Note this file must be included before
+// building system-messages-proto, otherwise it will not build by itself.
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
new file mode 100644
index 0000000..fd8397f
--- /dev/null
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -0,0 +1,10316 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.RECEIVE_DATA_ACTIVITY_CHANGE;
+import static android.content.pm.PackageManager.FEATURE_BLUETOOTH;
+import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.content.pm.PackageManager.FEATURE_WIFI;
+import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_ATTEMPTED_BITMASK;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_SUCCEEDED_BITMASK;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_DNS_EVENTS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_TCP_METRICS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
+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.TYPE_BLUETOOTH;
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_MOBILE_CBS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
+import static android.net.ConnectivityManager.TYPE_MOBILE_EMERGENCY;
+import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA;
+import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
+import static android.net.ConnectivityManager.TYPE_MOBILE_IA;
+import static android.net.ConnectivityManager.TYPE_MOBILE_IMS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_MMS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
+import static android.net.ConnectivityManager.TYPE_NONE;
+import static android.net.ConnectivityManager.TYPE_PROXY;
+import static android.net.ConnectivityManager.TYPE_VPN;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
+import static android.net.ConnectivityManager.getNetworkTypeName;
+import static android.net.ConnectivityManager.isNetworkTypeValid;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+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;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST;
+import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST;
+import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY;
+import static android.net.shared.NetworkMonitorUtils.isPrivateDnsValidationRequired;
+import static android.os.Process.INVALID_UID;
+import static android.os.Process.VPN_UID;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static java.util.Map.Entry;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
+import android.app.PendingIntent;
+import android.app.usage.NetworkStatsManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.net.CaptivePortal;
+import android.net.CaptivePortalData;
+import android.net.ConnectionInfo;
+import android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
+import android.net.ConnectivityDiagnosticsManager.DataStallReport;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.BlockedReason;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivityManager.RestrictBackgroundStatus;
+import android.net.ConnectivityResources;
+import android.net.ConnectivitySettingsManager;
+import android.net.DataStallReportParcelable;
+import android.net.DnsResolverServiceManager;
+import android.net.ICaptivePortal;
+import android.net.IConnectivityDiagnosticsCallback;
+import android.net.IConnectivityManager;
+import android.net.IDnsResolver;
+import android.net.INetd;
+import android.net.INetworkActivityListener;
+import android.net.INetworkAgent;
+import android.net.INetworkMonitor;
+import android.net.INetworkMonitorCallbacks;
+import android.net.INetworkOfferCallback;
+import android.net.IOnCompleteListener;
+import android.net.IQosCallback;
+import android.net.ISocketKeepaliveCallback;
+import android.net.InetAddresses;
+import android.net.IpMemoryStore;
+import android.net.IpPrefix;
+import android.net.LinkProperties;
+import android.net.MatchAllNetworkSpecifier;
+import android.net.NativeNetworkConfig;
+import android.net.NativeNetworkType;
+import android.net.NattSocketKeepalive;
+import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkMonitorManager;
+import android.net.NetworkPolicyManager;
+import android.net.NetworkPolicyManager.NetworkPolicyCallback;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.net.NetworkSpecifier;
+import android.net.NetworkStack;
+import android.net.NetworkState;
+import android.net.NetworkStateSnapshot;
+import android.net.NetworkTestResultParcelable;
+import android.net.NetworkUtils;
+import android.net.NetworkWatchlistManager;
+import android.net.OemNetworkPreferences;
+import android.net.PrivateDnsConfigParcel;
+import android.net.ProxyInfo;
+import android.net.QosCallbackException;
+import android.net.QosFilter;
+import android.net.QosSocketFilter;
+import android.net.QosSocketInfo;
+import android.net.RouteInfo;
+import android.net.RouteInfoParcel;
+import android.net.SocketKeepalive;
+import android.net.TetheringManager;
+import android.net.TransportInfo;
+import android.net.UidRange;
+import android.net.UidRangeParcel;
+import android.net.UnderlyingNetworkInfo;
+import android.net.Uri;
+import android.net.VpnManager;
+import android.net.VpnTransportInfo;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.NetworkEvent;
+import android.net.netlink.InetDiagMessage;
+import android.net.networkstack.ModuleNetworkStackClient;
+import android.net.networkstack.NetworkStackClientBase;
+import android.net.resolv.aidl.DnsHealthEventParcel;
+import android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener;
+import android.net.resolv.aidl.Nat64PrefixEventParcel;
+import android.net.resolv.aidl.PrivateDnsValidationEventParcel;
+import android.net.shared.PrivateDnsConfig;
+import android.net.util.MultinetworkPolicyTracker;
+import android.os.BatteryStatsManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.sysprop.NetworkProperties;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.LocalLog;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.MessageUtils;
+import com.android.modules.utils.BasicShellCommandHandler;
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult;
+import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
+import com.android.net.module.util.LocationPermissionChecker;
+import com.android.net.module.util.NetworkCapabilitiesUtils;
+import com.android.net.module.util.PermissionUtils;
+import com.android.server.connectivity.AutodestructReference;
+import com.android.server.connectivity.DnsManager;
+import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
+import com.android.server.connectivity.FullScore;
+import com.android.server.connectivity.KeepaliveTracker;
+import com.android.server.connectivity.LingerMonitor;
+import com.android.server.connectivity.MockableSystemProperties;
+import com.android.server.connectivity.NetworkAgentInfo;
+import com.android.server.connectivity.NetworkDiagnostics;
+import com.android.server.connectivity.NetworkNotificationManager;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+import com.android.server.connectivity.NetworkOffer;
+import com.android.server.connectivity.NetworkRanker;
+import com.android.server.connectivity.PermissionMonitor;
+import com.android.server.connectivity.ProfileNetworkPreferences;
+import com.android.server.connectivity.ProxyTracker;
+import com.android.server.connectivity.QosCallbackTracker;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.StringJoiner;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @hide
+ */
+public class ConnectivityService extends IConnectivityManager.Stub
+        implements PendingIntent.OnFinished {
+    private static final String TAG = ConnectivityService.class.getSimpleName();
+
+    private static final String DIAG_ARG = "--diag";
+    public static final String SHORT_ARG = "--short";
+    private static final String NETWORK_ARG = "networks";
+    private static final String REQUEST_ARG = "requests";
+
+    private static final boolean DBG = true;
+    private static final boolean DDBG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
+
+    private static final boolean LOGD_BLOCKED_NETWORKINFO = true;
+
+    /**
+     * Default URL to use for {@link #getCaptivePortalServerUrl()}. This should not be changed
+     * by OEMs for configuration purposes, as this value is overridden by
+     * ConnectivitySettingsManager.CAPTIVE_PORTAL_HTTP_URL.
+     * R.string.config_networkCaptivePortalServerUrl should be overridden instead for this purpose
+     * (preferably via runtime resource overlays).
+     */
+    private static final String DEFAULT_CAPTIVE_PORTAL_HTTP_URL =
+            "http://connectivitycheck.gstatic.com/generate_204";
+
+    // TODO: create better separation between radio types and network types
+
+    // how long to wait before switching back to a radio's default network
+    private static final int RESTORE_DEFAULT_NETWORK_DELAY = 1 * 60 * 1000;
+    // system property that can override the above value
+    private static final String NETWORK_RESTORE_DELAY_PROP_NAME =
+            "android.telephony.apn-restore";
+
+    // How long to wait before putting up a "This network doesn't have an Internet connection,
+    // connect anyway?" dialog after the user selects a network that doesn't validate.
+    private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000;
+
+    // Default to 30s linger time-out, and 5s for nascent network. Modifiable only for testing.
+    private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
+    private static final int DEFAULT_LINGER_DELAY_MS = 30_000;
+    private static final int DEFAULT_NASCENT_DELAY_MS = 5_000;
+
+    // The maximum number of network request allowed per uid before an exception is thrown.
+    private static final int MAX_NETWORK_REQUESTS_PER_UID = 100;
+
+    // The maximum number of network request allowed for system UIDs before an exception is thrown.
+    @VisibleForTesting
+    static final int MAX_NETWORK_REQUESTS_PER_SYSTEM_UID = 250;
+
+    @VisibleForTesting
+    protected int mLingerDelayMs;  // Can't be final, or test subclass constructors can't change it.
+    @VisibleForTesting
+    protected int mNascentDelayMs;
+
+    // How long to delay to removal of a pending intent based request.
+    // See ConnectivitySettingsManager.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS
+    private final int mReleasePendingIntentDelayMs;
+
+    private MockableSystemProperties mSystemProperties;
+
+    @VisibleForTesting
+    protected final PermissionMonitor mPermissionMonitor;
+
+    private final PerUidCounter mNetworkRequestCounter;
+    @VisibleForTesting
+    final PerUidCounter mSystemNetworkRequestCounter;
+
+    private volatile boolean mLockdownEnabled;
+
+    /**
+     * Stale copy of uid blocked reasons provided by NPMS. As long as they are accessed only in
+     * internal handler thread, they don't need a lock.
+     */
+    private SparseIntArray mUidBlockedReasons = new SparseIntArray();
+
+    private final Context mContext;
+    private final ConnectivityResources mResources;
+    // The Context is created for UserHandle.ALL.
+    private final Context mUserAllContext;
+    private final Dependencies mDeps;
+    // 0 is full bad, 100 is full good
+    private int mDefaultInetConditionPublished = 0;
+
+    @VisibleForTesting
+    protected IDnsResolver mDnsResolver;
+    @VisibleForTesting
+    protected INetd mNetd;
+    private NetworkStatsManager mStatsManager;
+    private NetworkPolicyManager mPolicyManager;
+    private final NetdCallback mNetdCallback;
+
+    /**
+     * TestNetworkService (lazily) created upon first usage. Locked to prevent creation of multiple
+     * instances.
+     */
+    @GuardedBy("mTNSLock")
+    private TestNetworkService mTNS;
+
+    private final Object mTNSLock = new Object();
+
+    private String mCurrentTcpBufferSizes;
+
+    private static final SparseArray<String> sMagicDecoderRing = MessageUtils.findMessageNames(
+            new Class[] { ConnectivityService.class, NetworkAgent.class, NetworkAgentInfo.class });
+
+    private enum ReapUnvalidatedNetworks {
+        // Tear down networks that have no chance (e.g. even if validated) of becoming
+        // the highest scoring network satisfying a NetworkRequest.  This should be passed when
+        // all networks have been rematched against all NetworkRequests.
+        REAP,
+        // Don't reap networks.  This should be passed when some networks have not yet been
+        // rematched against all NetworkRequests.
+        DONT_REAP
+    }
+
+    private enum UnneededFor {
+        LINGER,    // Determine whether this network is unneeded and should be lingered.
+        TEARDOWN,  // Determine whether this network is unneeded and should be torn down.
+    }
+
+    /**
+     * The priority value is used when issue uid ranges rules to netd. Netd will use the priority
+     * value and uid ranges to generate corresponding ip rules specific to the given preference.
+     * Thus, any device originated data traffic of the applied uids can be routed to the altered
+     * default network which has highest priority.
+     *
+     * Note: The priority value should be in 0~1000. Larger value means lower priority, see
+     *       {@link NativeUidRangeConfig}.
+     */
+    // This is default priority value for those NetworkRequests which doesn't have preference to
+    // alter default network and use the global one.
+    @VisibleForTesting
+    static final int DEFAULT_NETWORK_PRIORITY_NONE = 0;
+    // Used by automotive devices to set the network preferences used to direct traffic at an
+    // application level. See {@link #setOemNetworkPreference}.
+    @VisibleForTesting
+    static final int DEFAULT_NETWORK_PRIORITY_OEM = 10;
+    // Request that a user profile is put by default on a network matching a given preference.
+    // See {@link #setProfileNetworkPreference}.
+    @VisibleForTesting
+    static final int DEFAULT_NETWORK_PRIORITY_PROFILE = 20;
+    // Set by MOBILE_DATA_PREFERRED_UIDS setting. Use mobile data in preference even when
+    // higher-priority networks are connected.
+    @VisibleForTesting
+    static final int DEFAULT_NETWORK_PRIORITY_MOBILE_DATA_PREFERRED = 30;
+
+    /**
+     * used internally to clear a wakelock when transitioning
+     * from one net to another.  Clear happens when we get a new
+     * network - EVENT_EXPIRE_NET_TRANSITION_WAKELOCK happens
+     * after a timeout if no network is found (typically 1 min).
+     */
+    private static final int EVENT_CLEAR_NET_TRANSITION_WAKELOCK = 8;
+
+    /**
+     * used internally to reload global proxy settings
+     */
+    private static final int EVENT_APPLY_GLOBAL_HTTP_PROXY = 9;
+
+    /**
+     * PAC manager has received new port.
+     */
+    private static final int EVENT_PROXY_HAS_CHANGED = 16;
+
+    /**
+     * used internally when registering NetworkProviders
+     * obj = NetworkProviderInfo
+     */
+    private static final int EVENT_REGISTER_NETWORK_PROVIDER = 17;
+
+    /**
+     * used internally when registering NetworkAgents
+     * obj = Messenger
+     */
+    private static final int EVENT_REGISTER_NETWORK_AGENT = 18;
+
+    /**
+     * used to add a network request
+     * includes a NetworkRequestInfo
+     */
+    private static final int EVENT_REGISTER_NETWORK_REQUEST = 19;
+
+    /**
+     * indicates a timeout period is over - check if we had a network yet or not
+     * and if not, call the timeout callback (but leave the request live until they
+     * cancel it.
+     * includes a NetworkRequestInfo
+     */
+    private static final int EVENT_TIMEOUT_NETWORK_REQUEST = 20;
+
+    /**
+     * used to add a network listener - no request
+     * includes a NetworkRequestInfo
+     */
+    private static final int EVENT_REGISTER_NETWORK_LISTENER = 21;
+
+    /**
+     * used to remove a network request, either a listener or a real request
+     * arg1 = UID of caller
+     * obj  = NetworkRequest
+     */
+    private static final int EVENT_RELEASE_NETWORK_REQUEST = 22;
+
+    /**
+     * used internally when registering NetworkProviders
+     * obj = Messenger
+     */
+    private static final int EVENT_UNREGISTER_NETWORK_PROVIDER = 23;
+
+    /**
+     * used internally to expire a wakelock when transitioning
+     * from one net to another.  Expire happens when we fail to find
+     * a new network (typically after 1 minute) -
+     * EVENT_CLEAR_NET_TRANSITION_WAKELOCK happens if we had found
+     * a replacement network.
+     */
+    private static final int EVENT_EXPIRE_NET_TRANSITION_WAKELOCK = 24;
+
+    /**
+     * used to add a network request with a pending intent
+     * obj = NetworkRequestInfo
+     */
+    private static final int EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT = 26;
+
+    /**
+     * used to remove a pending intent and its associated network request.
+     * arg1 = UID of caller
+     * obj  = PendingIntent
+     */
+    private static final int EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT = 27;
+
+    /**
+     * used to specify whether a network should be used even if unvalidated.
+     * arg1 = whether to accept the network if it's unvalidated (1 or 0)
+     * arg2 = whether to remember this choice in the future (1 or 0)
+     * obj  = network
+     */
+    private static final int EVENT_SET_ACCEPT_UNVALIDATED = 28;
+
+    /**
+     * used to ask the user to confirm a connection to an unvalidated network.
+     * obj  = network
+     */
+    private static final int EVENT_PROMPT_UNVALIDATED = 29;
+
+    /**
+     * used internally to (re)configure always-on networks.
+     */
+    private static final int EVENT_CONFIGURE_ALWAYS_ON_NETWORKS = 30;
+
+    /**
+     * used to add a network listener with a pending intent
+     * obj = NetworkRequestInfo
+     */
+    private static final int EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT = 31;
+
+    /**
+     * used to specify whether a network should not be penalized when it becomes unvalidated.
+     */
+    private static final int EVENT_SET_AVOID_UNVALIDATED = 35;
+
+    /**
+     * used to trigger revalidation of a network.
+     */
+    private static final int EVENT_REVALIDATE_NETWORK = 36;
+
+    // Handle changes in Private DNS settings.
+    private static final int EVENT_PRIVATE_DNS_SETTINGS_CHANGED = 37;
+
+    // Handle private DNS validation status updates.
+    private static final int EVENT_PRIVATE_DNS_VALIDATION_UPDATE = 38;
+
+     /**
+      * Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the network has
+      * been tested.
+      * obj = {@link NetworkTestedResults} representing information sent from NetworkMonitor.
+      * data = PersistableBundle of extras passed from NetworkMonitor. If {@link
+      * NetworkMonitorCallbacks#notifyNetworkTested} is called, this will be null.
+      */
+    private static final int EVENT_NETWORK_TESTED = 41;
+
+    /**
+     * Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the private DNS
+     * config was resolved.
+     * obj = PrivateDnsConfig
+     * arg2 = netid
+     */
+    private static final int EVENT_PRIVATE_DNS_CONFIG_RESOLVED = 42;
+
+    /**
+     * Request ConnectivityService display provisioning notification.
+     * arg1    = Whether to make the notification visible.
+     * arg2    = NetID.
+     * obj     = Intent to be launched when notification selected by user, null if !arg1.
+     */
+    private static final int EVENT_PROVISIONING_NOTIFICATION = 43;
+
+    /**
+     * Used to specify whether a network should be used even if connectivity is partial.
+     * arg1 = whether to accept the network if its connectivity is partial (1 for true or 0 for
+     * false)
+     * arg2 = whether to remember this choice in the future (1 for true or 0 for false)
+     * obj  = network
+     */
+    private static final int EVENT_SET_ACCEPT_PARTIAL_CONNECTIVITY = 44;
+
+    /**
+     * Event for NetworkMonitor to inform ConnectivityService that the probe status has changed.
+     * Both of the arguments are bitmasks, and the value of bits come from
+     * INetworkMonitor.NETWORK_VALIDATION_PROBE_*.
+     * arg1 = A bitmask to describe which probes are completed.
+     * arg2 = A bitmask to describe which probes are successful.
+     */
+    public static final int EVENT_PROBE_STATUS_CHANGED = 45;
+
+    /**
+     * Event for NetworkMonitor to inform ConnectivityService that captive portal data has changed.
+     * arg1 = unused
+     * arg2 = netId
+     * obj = captive portal data
+     */
+    private static final int EVENT_CAPPORT_DATA_CHANGED = 46;
+
+    /**
+     * Used by setRequireVpnForUids.
+     * arg1 = whether the specified UID ranges are required to use a VPN.
+     * obj  = Array of UidRange objects.
+     */
+    private static final int EVENT_SET_REQUIRE_VPN_FOR_UIDS = 47;
+
+    /**
+     * Used internally when setting the default networks for OemNetworkPreferences.
+     * obj = Pair<OemNetworkPreferences, listener>
+     */
+    private static final int EVENT_SET_OEM_NETWORK_PREFERENCE = 48;
+
+    /**
+     * Used to indicate the system default network becomes active.
+     */
+    private static final int EVENT_REPORT_NETWORK_ACTIVITY = 49;
+
+    /**
+     * Used internally when setting a network preference for a user profile.
+     * obj = Pair<ProfileNetworkPreference, Listener>
+     */
+    private static final int EVENT_SET_PROFILE_NETWORK_PREFERENCE = 50;
+
+    /**
+     * Event to specify that reasons for why an uid is blocked changed.
+     * arg1 = uid
+     * arg2 = blockedReasons
+     */
+    private static final int EVENT_UID_BLOCKED_REASON_CHANGED = 51;
+
+    /**
+     * Event to register a new network offer
+     * obj = NetworkOffer
+     */
+    private static final int EVENT_REGISTER_NETWORK_OFFER = 52;
+
+    /**
+     * Event to unregister an existing network offer
+     * obj = INetworkOfferCallback
+     */
+    private static final int EVENT_UNREGISTER_NETWORK_OFFER = 53;
+
+    /**
+     * Used internally when MOBILE_DATA_PREFERRED_UIDS setting changed.
+     */
+    private static final int EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED = 54;
+
+    /**
+     * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
+     * should be shown.
+     */
+    private static final int PROVISIONING_NOTIFICATION_SHOW = 1;
+
+    /**
+     * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
+     * should be hidden.
+     */
+    private static final int PROVISIONING_NOTIFICATION_HIDE = 0;
+
+    private static String eventName(int what) {
+        return sMagicDecoderRing.get(what, Integer.toString(what));
+    }
+
+    private static IDnsResolver getDnsResolver(Context context) {
+        final DnsResolverServiceManager dsm = context.getSystemService(
+                DnsResolverServiceManager.class);
+        return IDnsResolver.Stub.asInterface(dsm.getService());
+    }
+
+    /** Handler thread used for all of the handlers below. */
+    @VisibleForTesting
+    protected final HandlerThread mHandlerThread;
+    /** Handler used for internal events. */
+    final private InternalHandler mHandler;
+    /** Handler used for incoming {@link NetworkStateTracker} events. */
+    final private NetworkStateTrackerHandler mTrackerHandler;
+    /** Handler used for processing {@link android.net.ConnectivityDiagnosticsManager} events */
+    @VisibleForTesting
+    final ConnectivityDiagnosticsHandler mConnectivityDiagnosticsHandler;
+
+    private final DnsManager mDnsManager;
+    private final NetworkRanker mNetworkRanker;
+
+    private boolean mSystemReady;
+    private Intent mInitialBroadcast;
+
+    private PowerManager.WakeLock mNetTransitionWakeLock;
+    private final PowerManager.WakeLock mPendingIntentWakeLock;
+
+    // A helper object to track the current default HTTP proxy. ConnectivityService needs to tell
+    // the world when it changes.
+    @VisibleForTesting
+    protected final ProxyTracker mProxyTracker;
+
+    final private SettingsObserver mSettingsObserver;
+
+    private UserManager mUserManager;
+
+    // the set of network types that can only be enabled by system/sig apps
+    private List<Integer> mProtectedNetworks;
+
+    private Set<String> mWolSupportedInterfaces;
+
+    private final TelephonyManager mTelephonyManager;
+    private final AppOpsManager mAppOpsManager;
+
+    private final LocationPermissionChecker mLocationPermissionChecker;
+
+    private KeepaliveTracker mKeepaliveTracker;
+    private QosCallbackTracker mQosCallbackTracker;
+    private NetworkNotificationManager mNotifier;
+    private LingerMonitor mLingerMonitor;
+
+    // sequence number of NetworkRequests
+    private int mNextNetworkRequestId = NetworkRequest.FIRST_REQUEST_ID;
+
+    // Sequence number for NetworkProvider IDs.
+    private final AtomicInteger mNextNetworkProviderId = new AtomicInteger(
+            NetworkProvider.FIRST_PROVIDER_ID);
+
+    // NetworkRequest activity String log entries.
+    private static final int MAX_NETWORK_REQUEST_LOGS = 20;
+    private final LocalLog mNetworkRequestInfoLogs = new LocalLog(MAX_NETWORK_REQUEST_LOGS);
+
+    // NetworkInfo blocked and unblocked String log entries
+    private static final int MAX_NETWORK_INFO_LOGS = 40;
+    private final LocalLog mNetworkInfoBlockingLogs = new LocalLog(MAX_NETWORK_INFO_LOGS);
+
+    private static final int MAX_WAKELOCK_LOGS = 20;
+    private final LocalLog mWakelockLogs = new LocalLog(MAX_WAKELOCK_LOGS);
+    private int mTotalWakelockAcquisitions = 0;
+    private int mTotalWakelockReleases = 0;
+    private long mTotalWakelockDurationMs = 0;
+    private long mMaxWakelockDurationMs = 0;
+    private long mLastWakeLockAcquireTimestamp = 0;
+
+    private final IpConnectivityLog mMetricsLog;
+
+    @GuardedBy("mBandwidthRequests")
+    private final SparseArray<Integer> mBandwidthRequests = new SparseArray(10);
+
+    @VisibleForTesting
+    final MultinetworkPolicyTracker mMultinetworkPolicyTracker;
+
+    @VisibleForTesting
+    final Map<IBinder, ConnectivityDiagnosticsCallbackInfo> mConnectivityDiagnosticsCallbacks =
+            new HashMap<>();
+
+    /**
+     * Implements support for the legacy "one network per network type" model.
+     *
+     * We used to have a static array of NetworkStateTrackers, one for each
+     * network type, but that doesn't work any more now that we can have,
+     * for example, more that one wifi network. This class stores all the
+     * NetworkAgentInfo objects that support a given type, but the legacy
+     * API will only see the first one.
+     *
+     * It serves two main purposes:
+     *
+     * 1. Provide information about "the network for a given type" (since this
+     *    API only supports one).
+     * 2. Send legacy connectivity change broadcasts. Broadcasts are sent if
+     *    the first network for a given type changes, or if the default network
+     *    changes.
+     */
+    @VisibleForTesting
+    static class LegacyTypeTracker {
+
+        private static final boolean DBG = true;
+        private static final boolean VDBG = false;
+
+        /**
+         * Array of lists, one per legacy network type (e.g., TYPE_MOBILE_MMS).
+         * Each list holds references to all NetworkAgentInfos that are used to
+         * satisfy requests for that network type.
+         *
+         * This array is built out at startup such that an unsupported network
+         * doesn't get an ArrayList instance, making this a tristate:
+         * unsupported, supported but not active and active.
+         *
+         * The actual lists are populated when we scan the network types that
+         * are supported on this device.
+         *
+         * Threading model:
+         *  - addSupportedType() is only called in the constructor
+         *  - add(), update(), remove() are only called from the ConnectivityService handler thread.
+         *    They are therefore not thread-safe with respect to each other.
+         *  - getNetworkForType() can be called at any time on binder threads. It is synchronized
+         *    on mTypeLists to be thread-safe with respect to a concurrent remove call.
+         *  - getRestoreTimerForType(type) is also synchronized on mTypeLists.
+         *  - dump is thread-safe with respect to concurrent add and remove calls.
+         */
+        private final ArrayList<NetworkAgentInfo> mTypeLists[];
+        @NonNull
+        private final ConnectivityService mService;
+
+        // Restore timers for requestNetworkForFeature (network type -> timer in ms). Types without
+        // an entry have no timer (equivalent to -1). Lazily loaded.
+        @NonNull
+        private ArrayMap<Integer, Integer> mRestoreTimers = new ArrayMap<>();
+
+        LegacyTypeTracker(@NonNull ConnectivityService service) {
+            mService = service;
+            mTypeLists = new ArrayList[ConnectivityManager.MAX_NETWORK_TYPE + 1];
+        }
+
+        public void loadSupportedTypes(@NonNull Context ctx, @NonNull TelephonyManager tm) {
+            final PackageManager pm = ctx.getPackageManager();
+            if (pm.hasSystemFeature(FEATURE_WIFI)) {
+                addSupportedType(TYPE_WIFI);
+            }
+            if (pm.hasSystemFeature(FEATURE_WIFI_DIRECT)) {
+                addSupportedType(TYPE_WIFI_P2P);
+            }
+            if (tm.isDataCapable()) {
+                // Telephony does not have granular support for these types: they are either all
+                // supported, or none is supported
+                addSupportedType(TYPE_MOBILE);
+                addSupportedType(TYPE_MOBILE_MMS);
+                addSupportedType(TYPE_MOBILE_SUPL);
+                addSupportedType(TYPE_MOBILE_DUN);
+                addSupportedType(TYPE_MOBILE_HIPRI);
+                addSupportedType(TYPE_MOBILE_FOTA);
+                addSupportedType(TYPE_MOBILE_IMS);
+                addSupportedType(TYPE_MOBILE_CBS);
+                addSupportedType(TYPE_MOBILE_IA);
+                addSupportedType(TYPE_MOBILE_EMERGENCY);
+            }
+            if (pm.hasSystemFeature(FEATURE_BLUETOOTH)) {
+                addSupportedType(TYPE_BLUETOOTH);
+            }
+            if (pm.hasSystemFeature(FEATURE_WATCH)) {
+                // TYPE_PROXY is only used on Wear
+                addSupportedType(TYPE_PROXY);
+            }
+            // Ethernet is often not specified in the configs, although many devices can use it via
+            // USB host adapters. Add it as long as the ethernet service is here.
+            if (ctx.getSystemService(Context.ETHERNET_SERVICE) != null) {
+                addSupportedType(TYPE_ETHERNET);
+            }
+
+            // Always add TYPE_VPN as a supported type
+            addSupportedType(TYPE_VPN);
+        }
+
+        private void addSupportedType(int type) {
+            if (mTypeLists[type] != null) {
+                throw new IllegalStateException(
+                        "legacy list for type " + type + "already initialized");
+            }
+            mTypeLists[type] = new ArrayList<>();
+        }
+
+        public boolean isTypeSupported(int type) {
+            return isNetworkTypeValid(type) && mTypeLists[type] != null;
+        }
+
+        public NetworkAgentInfo getNetworkForType(int type) {
+            synchronized (mTypeLists) {
+                if (isTypeSupported(type) && !mTypeLists[type].isEmpty()) {
+                    return mTypeLists[type].get(0);
+                }
+            }
+            return null;
+        }
+
+        public int getRestoreTimerForType(int type) {
+            synchronized (mTypeLists) {
+                if (mRestoreTimers == null) {
+                    mRestoreTimers = loadRestoreTimers();
+                }
+                return mRestoreTimers.getOrDefault(type, -1);
+            }
+        }
+
+        private ArrayMap<Integer, Integer> loadRestoreTimers() {
+            final String[] configs = mService.mResources.get().getStringArray(
+                    R.array.config_legacy_networktype_restore_timers);
+            final ArrayMap<Integer, Integer> ret = new ArrayMap<>(configs.length);
+            for (final String config : configs) {
+                final String[] splits = TextUtils.split(config, ",");
+                if (splits.length != 2) {
+                    logwtf("Invalid restore timer token count: " + config);
+                    continue;
+                }
+                try {
+                    ret.put(Integer.parseInt(splits[0]), Integer.parseInt(splits[1]));
+                } catch (NumberFormatException e) {
+                    logwtf("Invalid restore timer number format: " + config, e);
+                }
+            }
+            return ret;
+        }
+
+        private void maybeLogBroadcast(NetworkAgentInfo nai, DetailedState state, int type,
+                boolean isDefaultNetwork) {
+            if (DBG) {
+                log("Sending " + state
+                        + " broadcast for type " + type + " " + nai.toShortString()
+                        + " isDefaultNetwork=" + isDefaultNetwork);
+            }
+        }
+
+        // When a lockdown VPN connects, send another CONNECTED broadcast for the underlying
+        // network type, to preserve previous behaviour.
+        private void maybeSendLegacyLockdownBroadcast(@NonNull NetworkAgentInfo vpnNai) {
+            if (vpnNai != mService.getLegacyLockdownNai()) return;
+
+            if (vpnNai.declaredUnderlyingNetworks == null
+                    || vpnNai.declaredUnderlyingNetworks.length != 1) {
+                Log.wtf(TAG, "Legacy lockdown VPN must have exactly one underlying network: "
+                        + Arrays.toString(vpnNai.declaredUnderlyingNetworks));
+                return;
+            }
+            final NetworkAgentInfo underlyingNai = mService.getNetworkAgentInfoForNetwork(
+                    vpnNai.declaredUnderlyingNetworks[0]);
+            if (underlyingNai == null) return;
+
+            final int type = underlyingNai.networkInfo.getType();
+            final DetailedState state = DetailedState.CONNECTED;
+            maybeLogBroadcast(underlyingNai, state, type, true /* isDefaultNetwork */);
+            mService.sendLegacyNetworkBroadcast(underlyingNai, state, type);
+        }
+
+        /** Adds the given network to the specified legacy type list. */
+        public void add(int type, NetworkAgentInfo nai) {
+            if (!isTypeSupported(type)) {
+                return;  // Invalid network type.
+            }
+            if (VDBG) log("Adding agent " + nai + " for legacy network type " + type);
+
+            ArrayList<NetworkAgentInfo> list = mTypeLists[type];
+            if (list.contains(nai)) {
+                return;
+            }
+            synchronized (mTypeLists) {
+                list.add(nai);
+            }
+
+            // Send a broadcast if this is the first network of its type or if it's the default.
+            final boolean isDefaultNetwork = mService.isDefaultNetwork(nai);
+
+            // If a legacy lockdown VPN is active, override the NetworkInfo state in all broadcasts
+            // to preserve previous behaviour.
+            final DetailedState state = mService.getLegacyLockdownState(DetailedState.CONNECTED);
+            if ((list.size() == 1) || isDefaultNetwork) {
+                maybeLogBroadcast(nai, state, type, isDefaultNetwork);
+                mService.sendLegacyNetworkBroadcast(nai, state, type);
+            }
+
+            if (type == TYPE_VPN && state == DetailedState.CONNECTED) {
+                maybeSendLegacyLockdownBroadcast(nai);
+            }
+        }
+
+        /** Removes the given network from the specified legacy type list. */
+        public void remove(int type, NetworkAgentInfo nai, boolean wasDefault) {
+            ArrayList<NetworkAgentInfo> list = mTypeLists[type];
+            if (list == null || list.isEmpty()) {
+                return;
+            }
+            final boolean wasFirstNetwork = list.get(0).equals(nai);
+
+            synchronized (mTypeLists) {
+                if (!list.remove(nai)) {
+                    return;
+                }
+            }
+
+            if (wasFirstNetwork || wasDefault) {
+                maybeLogBroadcast(nai, DetailedState.DISCONNECTED, type, wasDefault);
+                mService.sendLegacyNetworkBroadcast(nai, DetailedState.DISCONNECTED, type);
+            }
+
+            if (!list.isEmpty() && wasFirstNetwork) {
+                if (DBG) log("Other network available for type " + type +
+                              ", sending connected broadcast");
+                final NetworkAgentInfo replacement = list.get(0);
+                maybeLogBroadcast(replacement, DetailedState.CONNECTED, type,
+                        mService.isDefaultNetwork(replacement));
+                mService.sendLegacyNetworkBroadcast(replacement, DetailedState.CONNECTED, type);
+            }
+        }
+
+        /** Removes the given network from all legacy type lists. */
+        public void remove(NetworkAgentInfo nai, boolean wasDefault) {
+            if (VDBG) log("Removing agent " + nai + " wasDefault=" + wasDefault);
+            for (int type = 0; type < mTypeLists.length; type++) {
+                remove(type, nai, wasDefault);
+            }
+        }
+
+        // send out another legacy broadcast - currently only used for suspend/unsuspend
+        // toggle
+        public void update(NetworkAgentInfo nai) {
+            final boolean isDefault = mService.isDefaultNetwork(nai);
+            final DetailedState state = nai.networkInfo.getDetailedState();
+            for (int type = 0; type < mTypeLists.length; type++) {
+                final ArrayList<NetworkAgentInfo> list = mTypeLists[type];
+                final boolean contains = (list != null && list.contains(nai));
+                final boolean isFirst = contains && (nai == list.get(0));
+                if (isFirst || contains && isDefault) {
+                    maybeLogBroadcast(nai, state, type, isDefault);
+                    mService.sendLegacyNetworkBroadcast(nai, state, type);
+                }
+            }
+        }
+
+        public void dump(IndentingPrintWriter pw) {
+            pw.println("mLegacyTypeTracker:");
+            pw.increaseIndent();
+            pw.print("Supported types:");
+            for (int type = 0; type < mTypeLists.length; type++) {
+                if (mTypeLists[type] != null) pw.print(" " + type);
+            }
+            pw.println();
+            pw.println("Current state:");
+            pw.increaseIndent();
+            synchronized (mTypeLists) {
+                for (int type = 0; type < mTypeLists.length; type++) {
+                    if (mTypeLists[type] == null || mTypeLists[type].isEmpty()) continue;
+                    for (NetworkAgentInfo nai : mTypeLists[type]) {
+                        pw.println(type + " " + nai.toShortString());
+                    }
+                }
+            }
+            pw.decreaseIndent();
+            pw.decreaseIndent();
+            pw.println();
+        }
+    }
+    private final LegacyTypeTracker mLegacyTypeTracker = new LegacyTypeTracker(this);
+
+    final LocalPriorityDump mPriorityDumper = new LocalPriorityDump();
+    /**
+     * Helper class which parses out priority arguments and dumps sections according to their
+     * priority. If priority arguments are omitted, function calls the legacy dump command.
+     */
+    private class LocalPriorityDump {
+        private static final String PRIORITY_ARG = "--dump-priority";
+        private static final String PRIORITY_ARG_HIGH = "HIGH";
+        private static final String PRIORITY_ARG_NORMAL = "NORMAL";
+
+        LocalPriorityDump() {}
+
+        private void dumpHigh(FileDescriptor fd, PrintWriter pw) {
+            doDump(fd, pw, new String[] {DIAG_ARG});
+            doDump(fd, pw, new String[] {SHORT_ARG});
+        }
+
+        private void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args) {
+            doDump(fd, pw, args);
+        }
+
+        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            if (args == null) {
+                dumpNormal(fd, pw, args);
+                return;
+            }
+
+            String priority = null;
+            for (int argIndex = 0; argIndex < args.length; argIndex++) {
+                if (args[argIndex].equals(PRIORITY_ARG) && argIndex + 1 < args.length) {
+                    argIndex++;
+                    priority = args[argIndex];
+                }
+            }
+
+            if (PRIORITY_ARG_HIGH.equals(priority)) {
+                dumpHigh(fd, pw);
+            } else if (PRIORITY_ARG_NORMAL.equals(priority)) {
+                dumpNormal(fd, pw, args);
+            } else {
+                // ConnectivityService publishes binder service using publishBinderService() with
+                // no priority assigned will be treated as NORMAL priority. Dumpsys does not send
+                // "--dump-priority" arguments to the service. Thus, dump NORMAL only to align the
+                // legacy output for dumpsys connectivity.
+                // TODO: Integrate into signal dump.
+                dumpNormal(fd, pw, args);
+            }
+        }
+    }
+
+    /**
+     * Keeps track of the number of requests made under different uids.
+     */
+    public static class PerUidCounter {
+        private final int mMaxCountPerUid;
+
+        // Map from UID to number of NetworkRequests that UID has filed.
+        @VisibleForTesting
+        @GuardedBy("mUidToNetworkRequestCount")
+        final SparseIntArray mUidToNetworkRequestCount = new SparseIntArray();
+
+        /**
+         * Constructor
+         *
+         * @param maxCountPerUid the maximum count per uid allowed
+         */
+        public PerUidCounter(final int maxCountPerUid) {
+            mMaxCountPerUid = maxCountPerUid;
+        }
+
+        /**
+         * Increments the request count of the given uid.  Throws an exception if the number
+         * of open requests for the uid exceeds the value of maxCounterPerUid which is the value
+         * passed into the constructor. see: {@link #PerUidCounter(int)}.
+         *
+         * @throws ServiceSpecificException with
+         * {@link ConnectivityManager.Errors.TOO_MANY_REQUESTS} if the number of requests for
+         * the uid exceed the allowed number.
+         *
+         * @param uid the uid that the request was made under
+         */
+        public void incrementCountOrThrow(final int uid) {
+            synchronized (mUidToNetworkRequestCount) {
+                incrementCountOrThrow(uid, 1 /* numToIncrement */);
+            }
+        }
+
+        private void incrementCountOrThrow(final int uid, final int numToIncrement) {
+            final int newRequestCount =
+                    mUidToNetworkRequestCount.get(uid, 0) + numToIncrement;
+            if (newRequestCount >= mMaxCountPerUid) {
+                throw new ServiceSpecificException(
+                        ConnectivityManager.Errors.TOO_MANY_REQUESTS);
+            }
+            mUidToNetworkRequestCount.put(uid, newRequestCount);
+        }
+
+        /**
+         * Decrements the request count of the given uid.
+         *
+         * @param uid the uid that the request was made under
+         */
+        public void decrementCount(final int uid) {
+            synchronized (mUidToNetworkRequestCount) {
+                decrementCount(uid, 1 /* numToDecrement */);
+            }
+        }
+
+        private void decrementCount(final int uid, final int numToDecrement) {
+            final int newRequestCount =
+                    mUidToNetworkRequestCount.get(uid, 0) - numToDecrement;
+            if (newRequestCount < 0) {
+                logwtf("BUG: too small request count " + newRequestCount + " for UID " + uid);
+            } else if (newRequestCount == 0) {
+                mUidToNetworkRequestCount.delete(uid);
+            } else {
+                mUidToNetworkRequestCount.put(uid, newRequestCount);
+            }
+        }
+
+        /**
+         * Used to adjust the request counter for the per-app API flows. Directly adjusting the
+         * counter is not ideal however in the per-app flows, the nris can't be removed until they
+         * are used to create the new nris upon set. Therefore the request count limit can be
+         * artificially hit. This method is used as a workaround for this particular case so that
+         * the request counts are accounted for correctly.
+         * @param uid the uid to adjust counts for
+         * @param numOfNewRequests the new request count to account for
+         * @param r the runnable to execute
+         */
+        public void transact(final int uid, final int numOfNewRequests, @NonNull final Runnable r) {
+            // This should only be used on the handler thread as per all current and foreseen
+            // use-cases. ensureRunningOnConnectivityServiceThread() can't be used because there is
+            // no ref to the outer ConnectivityService.
+            synchronized (mUidToNetworkRequestCount) {
+                final int reqCountOverage = getCallingUidRequestCountOverage(uid, numOfNewRequests);
+                decrementCount(uid, reqCountOverage);
+                r.run();
+                incrementCountOrThrow(uid, reqCountOverage);
+            }
+        }
+
+        private int getCallingUidRequestCountOverage(final int uid, final int numOfNewRequests) {
+            final int newUidRequestCount = mUidToNetworkRequestCount.get(uid, 0)
+                    + numOfNewRequests;
+            return newUidRequestCount >= MAX_NETWORK_REQUESTS_PER_SYSTEM_UID
+                    ? newUidRequestCount - (MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - 1) : 0;
+        }
+    }
+
+    /**
+     * Dependencies of ConnectivityService, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        public int getCallingUid() {
+            return Binder.getCallingUid();
+        }
+
+        /**
+         * Get system properties to use in ConnectivityService.
+         */
+        public MockableSystemProperties getSystemProperties() {
+            return new MockableSystemProperties();
+        }
+
+        /**
+         * Get the {@link ConnectivityResources} to use in ConnectivityService.
+         */
+        public ConnectivityResources getResources(@NonNull Context ctx) {
+            return new ConnectivityResources(ctx);
+        }
+
+        /**
+         * Create a HandlerThread to use in ConnectivityService.
+         */
+        public HandlerThread makeHandlerThread() {
+            return new HandlerThread("ConnectivityServiceThread");
+        }
+
+        /**
+         * Get a reference to the ModuleNetworkStackClient.
+         */
+        public NetworkStackClientBase getNetworkStack() {
+            return ModuleNetworkStackClient.getInstance(null);
+        }
+
+        /**
+         * @see ProxyTracker
+         */
+        public ProxyTracker makeProxyTracker(@NonNull Context context,
+                @NonNull Handler connServiceHandler) {
+            return new ProxyTracker(context, connServiceHandler, EVENT_PROXY_HAS_CHANGED);
+        }
+
+        /**
+         * @see NetIdManager
+         */
+        public NetIdManager makeNetIdManager() {
+            return new NetIdManager();
+        }
+
+        /**
+         * @see NetworkUtils#queryUserAccess(int, int)
+         */
+        public boolean queryUserAccess(int uid, Network network, ConnectivityService cs) {
+            return cs.queryUserAccess(uid, network);
+        }
+
+        /**
+         * Gets the UID that owns a socket connection. Needed because opening SOCK_DIAG sockets
+         * requires CAP_NET_ADMIN, which the unit tests do not have.
+         */
+        public int getConnectionOwnerUid(int protocol, InetSocketAddress local,
+                InetSocketAddress remote) {
+            return InetDiagMessage.getConnectionOwnerUid(protocol, local, remote);
+        }
+
+        /**
+         * @see MultinetworkPolicyTracker
+         */
+        public MultinetworkPolicyTracker makeMultinetworkPolicyTracker(
+                @NonNull Context c, @NonNull Handler h, @NonNull Runnable r) {
+            return new MultinetworkPolicyTracker(c, h, r);
+        }
+
+        /**
+         * @see BatteryStatsManager
+         */
+        public void reportNetworkInterfaceForTransports(Context context, String iface,
+                int[] transportTypes) {
+            final BatteryStatsManager batteryStats =
+                    context.getSystemService(BatteryStatsManager.class);
+            batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes);
+        }
+
+        public boolean getCellular464XlatEnabled() {
+            return NetworkProperties.isCellular464XlatEnabled().orElse(true);
+        }
+
+        /**
+         * @see PendingIntent#intentFilterEquals
+         */
+        public boolean intentFilterEquals(PendingIntent a, PendingIntent b) {
+            return a.intentFilterEquals(b);
+        }
+
+        /**
+         * @see LocationPermissionChecker
+         */
+        public LocationPermissionChecker makeLocationPermissionChecker(Context context) {
+            return new LocationPermissionChecker(context);
+        }
+    }
+
+    public ConnectivityService(Context context) {
+        this(context, getDnsResolver(context), new IpConnectivityLog(),
+                INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
+                new Dependencies());
+    }
+
+    @VisibleForTesting
+    protected ConnectivityService(Context context, IDnsResolver dnsresolver,
+            IpConnectivityLog logger, INetd netd, Dependencies deps) {
+        if (DBG) log("ConnectivityService starting up");
+
+        mDeps = Objects.requireNonNull(deps, "missing Dependencies");
+        mSystemProperties = mDeps.getSystemProperties();
+        mNetIdManager = mDeps.makeNetIdManager();
+        mContext = Objects.requireNonNull(context, "missing Context");
+        mResources = deps.getResources(mContext);
+        mNetworkRequestCounter = new PerUidCounter(MAX_NETWORK_REQUESTS_PER_UID);
+        mSystemNetworkRequestCounter = new PerUidCounter(MAX_NETWORK_REQUESTS_PER_SYSTEM_UID);
+
+        mMetricsLog = logger;
+        mNetworkRanker = new NetworkRanker();
+        final NetworkRequest defaultInternetRequest = createDefaultRequest();
+        mDefaultRequest = new NetworkRequestInfo(
+                Process.myUid(), defaultInternetRequest, null,
+                new Binder(), NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
+                null /* attributionTags */);
+        mNetworkRequests.put(defaultInternetRequest, mDefaultRequest);
+        mDefaultNetworkRequests.add(mDefaultRequest);
+        mNetworkRequestInfoLogs.log("REGISTER " + mDefaultRequest);
+
+        mDefaultMobileDataRequest = createDefaultInternetRequestForTransport(
+                NetworkCapabilities.TRANSPORT_CELLULAR, NetworkRequest.Type.BACKGROUND_REQUEST);
+
+        // The default WiFi request is a background request so that apps using WiFi are
+        // migrated to a better network (typically ethernet) when one comes up, instead
+        // of staying on WiFi forever.
+        mDefaultWifiRequest = createDefaultInternetRequestForTransport(
+                NetworkCapabilities.TRANSPORT_WIFI, NetworkRequest.Type.BACKGROUND_REQUEST);
+
+        mDefaultVehicleRequest = createAlwaysOnRequestForCapability(
+                NetworkCapabilities.NET_CAPABILITY_VEHICLE_INTERNAL,
+                NetworkRequest.Type.BACKGROUND_REQUEST);
+
+        mHandlerThread = mDeps.makeHandlerThread();
+        mHandlerThread.start();
+        mHandler = new InternalHandler(mHandlerThread.getLooper());
+        mTrackerHandler = new NetworkStateTrackerHandler(mHandlerThread.getLooper());
+        mConnectivityDiagnosticsHandler =
+                new ConnectivityDiagnosticsHandler(mHandlerThread.getLooper());
+
+        mReleasePendingIntentDelayMs = Settings.Secure.getInt(context.getContentResolver(),
+                ConnectivitySettingsManager.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, 5_000);
+
+        mLingerDelayMs = mSystemProperties.getInt(LINGER_DELAY_PROPERTY, DEFAULT_LINGER_DELAY_MS);
+        // TODO: Consider making the timer customizable.
+        mNascentDelayMs = DEFAULT_NASCENT_DELAY_MS;
+
+        mStatsManager = mContext.getSystemService(NetworkStatsManager.class);
+        mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);
+        mDnsResolver = Objects.requireNonNull(dnsresolver, "missing IDnsResolver");
+        mProxyTracker = mDeps.makeProxyTracker(mContext, mHandler);
+
+        mNetd = netd;
+        mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+        mLocationPermissionChecker = mDeps.makeLocationPermissionChecker(mContext);
+
+        // To ensure uid state is synchronized with Network Policy, register for
+        // NetworkPolicyManagerService events must happen prior to NetworkPolicyManagerService
+        // reading existing policy from disk.
+        mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);
+
+        final PowerManager powerManager = (PowerManager) context.getSystemService(
+                Context.POWER_SERVICE);
+        mNetTransitionWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+        mPendingIntentWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+
+        mLegacyTypeTracker.loadSupportedTypes(mContext, mTelephonyManager);
+        mProtectedNetworks = new ArrayList<>();
+        int[] protectedNetworks = mResources.get().getIntArray(R.array.config_protectedNetworks);
+        for (int p : protectedNetworks) {
+            if (mLegacyTypeTracker.isTypeSupported(p) && !mProtectedNetworks.contains(p)) {
+                mProtectedNetworks.add(p);
+            } else {
+                if (DBG) loge("Ignoring protectedNetwork " + p);
+            }
+        }
+
+        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+
+        mPermissionMonitor = new PermissionMonitor(mContext, mNetd);
+
+        mUserAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */);
+        // Listen for user add/removes to inform PermissionMonitor.
+        // Should run on mHandler to avoid any races.
+        final IntentFilter userIntentFilter = new IntentFilter();
+        userIntentFilter.addAction(Intent.ACTION_USER_ADDED);
+        userIntentFilter.addAction(Intent.ACTION_USER_REMOVED);
+        mUserAllContext.registerReceiver(mUserIntentReceiver, userIntentFilter,
+                null /* broadcastPermission */, mHandler);
+
+        // Listen to package add/removes for netd
+        final IntentFilter packageIntentFilter = new IntentFilter();
+        packageIntentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+        packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+        packageIntentFilter.addDataScheme("package");
+        mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter,
+                null /* broadcastPermission */, mHandler);
+
+        mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mHandler, mNetd);
+
+        mNetdCallback = new NetdCallback();
+        try {
+            mNetd.registerUnsolicitedEventListener(mNetdCallback);
+        } catch (RemoteException | ServiceSpecificException e) {
+            loge("Error registering event listener :" + e);
+        }
+
+        mSettingsObserver = new SettingsObserver(mContext, mHandler);
+        registerSettingsCallbacks();
+
+        mKeepaliveTracker = new KeepaliveTracker(mContext, mHandler);
+        mNotifier = new NetworkNotificationManager(mContext, mTelephonyManager);
+        mQosCallbackTracker = new QosCallbackTracker(mHandler, mNetworkRequestCounter);
+
+        final int dailyLimit = Settings.Global.getInt(mContext.getContentResolver(),
+                ConnectivitySettingsManager.NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT,
+                LingerMonitor.DEFAULT_NOTIFICATION_DAILY_LIMIT);
+        final long rateLimit = Settings.Global.getLong(mContext.getContentResolver(),
+                ConnectivitySettingsManager.NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS,
+                LingerMonitor.DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS);
+        mLingerMonitor = new LingerMonitor(mContext, mNotifier, dailyLimit, rateLimit);
+
+        mMultinetworkPolicyTracker = mDeps.makeMultinetworkPolicyTracker(
+                mContext, mHandler, () -> updateAvoidBadWifi());
+        mMultinetworkPolicyTracker.start();
+
+        mDnsManager = new DnsManager(mContext, mDnsResolver);
+        registerPrivateDnsSettingsCallbacks();
+
+        // This NAI is a sentinel used to offer no service to apps that are on a multi-layer
+        // request that doesn't allow fallback to the default network. It should never be visible
+        // to apps. As such, it's not in the list of NAIs and doesn't need many of the normal
+        // arguments like the handler or the DnsResolver.
+        // TODO : remove this ; it is probably better handled with a sentinel request.
+        mNoServiceNetwork = new NetworkAgentInfo(null,
+                new Network(INetd.UNREACHABLE_NET_ID),
+                new NetworkInfo(TYPE_NONE, 0, "", ""),
+                new LinkProperties(), new NetworkCapabilities(),
+                new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null,
+                new NetworkAgentConfig(), this, null, null, 0, INVALID_UID,
+                mLingerDelayMs, mQosCallbackTracker, mDeps);
+    }
+
+    private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) {
+        return createDefaultNetworkCapabilitiesForUidRange(new UidRange(uid, uid));
+    }
+
+    private static NetworkCapabilities createDefaultNetworkCapabilitiesForUidRange(
+            @NonNull final UidRange uids) {
+        final NetworkCapabilities netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_INTERNET);
+        netCap.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        netCap.removeCapability(NET_CAPABILITY_NOT_VPN);
+        netCap.setUids(UidRange.toIntRanges(Collections.singleton(uids)));
+        return netCap;
+    }
+
+    private NetworkRequest createDefaultRequest() {
+        return createDefaultInternetRequestForTransport(
+                TYPE_NONE, NetworkRequest.Type.REQUEST);
+    }
+
+    private NetworkRequest createDefaultInternetRequestForTransport(
+            int transportType, NetworkRequest.Type type) {
+        final NetworkCapabilities netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_INTERNET);
+        netCap.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        netCap.setRequestorUidAndPackageName(Process.myUid(), mContext.getPackageName());
+        if (transportType > TYPE_NONE) {
+            netCap.addTransportType(transportType);
+        }
+        return createNetworkRequest(type, netCap);
+    }
+
+    private NetworkRequest createNetworkRequest(
+            NetworkRequest.Type type, NetworkCapabilities netCap) {
+        return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId(), type);
+    }
+
+    private NetworkRequest createAlwaysOnRequestForCapability(int capability,
+            NetworkRequest.Type type) {
+        final NetworkCapabilities netCap = new NetworkCapabilities();
+        netCap.clearAll();
+        netCap.addCapability(capability);
+        netCap.setRequestorUidAndPackageName(Process.myUid(), mContext.getPackageName());
+        return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId(), type);
+    }
+
+    // Used only for testing.
+    // TODO: Delete this and either:
+    // 1. Give FakeSettingsProvider the ability to send settings change notifications (requires
+    //    changing ContentResolver to make registerContentObserver non-final).
+    // 2. Give FakeSettingsProvider an alternative notification mechanism and have the test use it
+    //    by subclassing SettingsObserver.
+    @VisibleForTesting
+    void updateAlwaysOnNetworks() {
+        mHandler.sendEmptyMessage(EVENT_CONFIGURE_ALWAYS_ON_NETWORKS);
+    }
+
+    // See FakeSettingsProvider comment above.
+    @VisibleForTesting
+    void updatePrivateDnsSettings() {
+        mHandler.sendEmptyMessage(EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
+    }
+
+    @VisibleForTesting
+    void updateMobileDataPreferredUids() {
+        mHandler.sendEmptyMessage(EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED);
+    }
+
+    private void handleAlwaysOnNetworkRequest(NetworkRequest networkRequest, int id) {
+        final boolean enable = mContext.getResources().getBoolean(id);
+        handleAlwaysOnNetworkRequest(networkRequest, enable);
+    }
+
+    private void handleAlwaysOnNetworkRequest(
+            NetworkRequest networkRequest, String settingName, boolean defaultValue) {
+        final boolean enable = toBool(Settings.Global.getInt(
+                mContext.getContentResolver(), settingName, encodeBool(defaultValue)));
+        handleAlwaysOnNetworkRequest(networkRequest, enable);
+    }
+
+    private void handleAlwaysOnNetworkRequest(NetworkRequest networkRequest, boolean enable) {
+        final boolean isEnabled = (mNetworkRequests.get(networkRequest) != null);
+        if (enable == isEnabled) {
+            return;  // Nothing to do.
+        }
+
+        if (enable) {
+            handleRegisterNetworkRequest(new NetworkRequestInfo(
+                    Process.myUid(), networkRequest, null, new Binder(),
+                    NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
+                    null /* attributionTags */));
+        } else {
+            handleReleaseNetworkRequest(networkRequest, Process.SYSTEM_UID,
+                    /* callOnUnavailable */ false);
+        }
+    }
+
+    private void handleConfigureAlwaysOnNetworks() {
+        handleAlwaysOnNetworkRequest(mDefaultMobileDataRequest,
+                ConnectivitySettingsManager.MOBILE_DATA_ALWAYS_ON, true /* defaultValue */);
+        handleAlwaysOnNetworkRequest(mDefaultWifiRequest,
+                ConnectivitySettingsManager.WIFI_ALWAYS_REQUESTED, false /* defaultValue */);
+        final boolean vehicleAlwaysRequested = mResources.get().getBoolean(
+                R.bool.config_vehicleInternalNetworkAlwaysRequested);
+        // TODO (b/183076074): remove legacy fallback after migrating overlays
+        final boolean legacyAlwaysRequested = mContext.getResources().getBoolean(
+                mContext.getResources().getIdentifier(
+                        "config_vehicleInternalNetworkAlwaysRequested", "bool", "android"));
+        handleAlwaysOnNetworkRequest(mDefaultVehicleRequest,
+                vehicleAlwaysRequested || legacyAlwaysRequested);
+    }
+
+    // Note that registering observer for setting do not get initial callback when registering,
+    // callers must fetch the initial value of the setting themselves if needed.
+    private void registerSettingsCallbacks() {
+        // Watch for global HTTP proxy changes.
+        mSettingsObserver.observe(
+                Settings.Global.getUriFor(Settings.Global.HTTP_PROXY),
+                EVENT_APPLY_GLOBAL_HTTP_PROXY);
+
+        // Watch for whether or not to keep mobile data always on.
+        mSettingsObserver.observe(
+                Settings.Global.getUriFor(ConnectivitySettingsManager.MOBILE_DATA_ALWAYS_ON),
+                EVENT_CONFIGURE_ALWAYS_ON_NETWORKS);
+
+        // Watch for whether or not to keep wifi always on.
+        mSettingsObserver.observe(
+                Settings.Global.getUriFor(ConnectivitySettingsManager.WIFI_ALWAYS_REQUESTED),
+                EVENT_CONFIGURE_ALWAYS_ON_NETWORKS);
+
+        // Watch for mobile data preferred uids changes.
+        mSettingsObserver.observe(
+                Settings.Secure.getUriFor(ConnectivitySettingsManager.MOBILE_DATA_PREFERRED_UIDS),
+                EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED);
+    }
+
+    private void registerPrivateDnsSettingsCallbacks() {
+        for (Uri uri : DnsManager.getPrivateDnsSettingsUris()) {
+            mSettingsObserver.observe(uri, EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
+        }
+    }
+
+    private synchronized int nextNetworkRequestId() {
+        // TODO: Consider handle wrapping and exclude {@link NetworkRequest#REQUEST_ID_NONE} if
+        //  doing that.
+        return mNextNetworkRequestId++;
+    }
+
+    @VisibleForTesting
+    protected NetworkAgentInfo getNetworkAgentInfoForNetwork(Network network) {
+        if (network == null) {
+            return null;
+        }
+        return getNetworkAgentInfoForNetId(network.getNetId());
+    }
+
+    private NetworkAgentInfo getNetworkAgentInfoForNetId(int netId) {
+        synchronized (mNetworkForNetId) {
+            return mNetworkForNetId.get(netId);
+        }
+    }
+
+    // TODO: determine what to do when more than one VPN applies to |uid|.
+    private NetworkAgentInfo getVpnForUid(int uid) {
+        synchronized (mNetworkForNetId) {
+            for (int i = 0; i < mNetworkForNetId.size(); i++) {
+                final NetworkAgentInfo nai = mNetworkForNetId.valueAt(i);
+                if (nai.isVPN() && nai.everConnected && nai.networkCapabilities.appliesToUid(uid)) {
+                    return nai;
+                }
+            }
+        }
+        return null;
+    }
+
+    private Network[] getVpnUnderlyingNetworks(int uid) {
+        if (mLockdownEnabled) return null;
+        final NetworkAgentInfo nai = getVpnForUid(uid);
+        if (nai != null) return nai.declaredUnderlyingNetworks;
+        return null;
+    }
+
+    private NetworkAgentInfo getNetworkAgentInfoForUid(int uid) {
+        NetworkAgentInfo nai = getDefaultNetworkForUid(uid);
+
+        final Network[] networks = getVpnUnderlyingNetworks(uid);
+        if (networks != null) {
+            // getUnderlyingNetworks() returns:
+            // null => there was no VPN, or the VPN didn't specify anything, so we use the default.
+            // empty array => the VPN explicitly said "no default network".
+            // non-empty array => the VPN specified one or more default networks; we use the
+            //                    first one.
+            if (networks.length > 0) {
+                nai = getNetworkAgentInfoForNetwork(networks[0]);
+            } else {
+                nai = null;
+            }
+        }
+        return nai;
+    }
+
+    /**
+     * Check if UID should be blocked from using the specified network.
+     */
+    private boolean isNetworkWithCapabilitiesBlocked(@Nullable final NetworkCapabilities nc,
+            final int uid, final boolean ignoreBlocked) {
+        // Networks aren't blocked when ignoring blocked status
+        if (ignoreBlocked) {
+            return false;
+        }
+        if (isUidBlockedByVpn(uid, mVpnBlockedUidRanges)) return true;
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            final boolean metered = nc == null ? true : nc.isMetered();
+            return mPolicyManager.isUidNetworkingBlocked(uid, metered);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private void maybeLogBlockedNetworkInfo(NetworkInfo ni, int uid) {
+        if (ni == null || !LOGD_BLOCKED_NETWORKINFO) {
+            return;
+        }
+        final boolean blocked;
+        synchronized (mBlockedAppUids) {
+            if (ni.getDetailedState() == DetailedState.BLOCKED && mBlockedAppUids.add(uid)) {
+                blocked = true;
+            } else if (ni.isConnected() && mBlockedAppUids.remove(uid)) {
+                blocked = false;
+            } else {
+                return;
+            }
+        }
+        String action = blocked ? "BLOCKED" : "UNBLOCKED";
+        log(String.format("Returning %s NetworkInfo to uid=%d", action, uid));
+        mNetworkInfoBlockingLogs.log(action + " " + uid);
+    }
+
+    private void maybeLogBlockedStatusChanged(NetworkRequestInfo nri, Network net, int blocked) {
+        if (nri == null || net == null || !LOGD_BLOCKED_NETWORKINFO) {
+            return;
+        }
+        final String action = (blocked != 0) ? "BLOCKED" : "UNBLOCKED";
+        final int requestId = nri.getActiveRequest() != null
+                ? nri.getActiveRequest().requestId : nri.mRequests.get(0).requestId;
+        mNetworkInfoBlockingLogs.log(String.format(
+                "%s %d(%d) on netId %d: %s", action, nri.mAsUid, requestId, net.getNetId(),
+                Integer.toHexString(blocked)));
+    }
+
+    /**
+     * Apply any relevant filters to the specified {@link NetworkInfo} for the given UID. For
+     * example, this may mark the network as {@link DetailedState#BLOCKED} based
+     * on {@link #isNetworkWithCapabilitiesBlocked}.
+     */
+    @NonNull
+    private NetworkInfo filterNetworkInfo(@NonNull NetworkInfo networkInfo, int type,
+            @NonNull NetworkCapabilities nc, int uid, boolean ignoreBlocked) {
+        final NetworkInfo filtered = new NetworkInfo(networkInfo);
+        // Many legacy types (e.g,. TYPE_MOBILE_HIPRI) are not actually a property of the network
+        // but only exists if an app asks about them or requests them. Ensure the requesting app
+        // gets the type it asks for.
+        filtered.setType(type);
+        if (isNetworkWithCapabilitiesBlocked(nc, uid, ignoreBlocked)) {
+            filtered.setDetailedState(DetailedState.BLOCKED, null /* reason */,
+                    null /* extraInfo */);
+        }
+        filterForLegacyLockdown(filtered);
+        return filtered;
+    }
+
+    private NetworkInfo getFilteredNetworkInfo(NetworkAgentInfo nai, int uid,
+            boolean ignoreBlocked) {
+        return filterNetworkInfo(nai.networkInfo, nai.networkInfo.getType(),
+                nai.networkCapabilities, uid, ignoreBlocked);
+    }
+
+    /**
+     * Return NetworkInfo for the active (i.e., connected) network interface.
+     * It is assumed that at most one network is active at a time. If more
+     * than one is active, it is indeterminate which will be returned.
+     * @return the info for the active network, or {@code null} if none is
+     * active
+     */
+    @Override
+    public NetworkInfo getActiveNetworkInfo() {
+        enforceAccessPermission();
+        final int uid = mDeps.getCallingUid();
+        final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid);
+        if (nai == null) return null;
+        final NetworkInfo networkInfo = getFilteredNetworkInfo(nai, uid, false);
+        maybeLogBlockedNetworkInfo(networkInfo, uid);
+        return networkInfo;
+    }
+
+    @Override
+    public Network getActiveNetwork() {
+        enforceAccessPermission();
+        return getActiveNetworkForUidInternal(mDeps.getCallingUid(), false);
+    }
+
+    @Override
+    public Network getActiveNetworkForUid(int uid, boolean ignoreBlocked) {
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+        return getActiveNetworkForUidInternal(uid, ignoreBlocked);
+    }
+
+    private Network getActiveNetworkForUidInternal(final int uid, boolean ignoreBlocked) {
+        final NetworkAgentInfo vpnNai = getVpnForUid(uid);
+        if (vpnNai != null) {
+            final NetworkCapabilities requiredCaps = createDefaultNetworkCapabilitiesForUid(uid);
+            if (requiredCaps.satisfiedByNetworkCapabilities(vpnNai.networkCapabilities)) {
+                return vpnNai.network;
+            }
+        }
+
+        NetworkAgentInfo nai = getDefaultNetworkForUid(uid);
+        if (nai == null || isNetworkWithCapabilitiesBlocked(nai.networkCapabilities, uid,
+                ignoreBlocked)) {
+            return null;
+        }
+        return nai.network;
+    }
+
+    @Override
+    public NetworkInfo getActiveNetworkInfoForUid(int uid, boolean ignoreBlocked) {
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+        final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid);
+        if (nai == null) return null;
+        return getFilteredNetworkInfo(nai, uid, ignoreBlocked);
+    }
+
+    /** Returns a NetworkInfo object for a network that doesn't exist. */
+    private NetworkInfo makeFakeNetworkInfo(int networkType, int uid) {
+        final NetworkInfo info = new NetworkInfo(networkType, 0 /* subtype */,
+                getNetworkTypeName(networkType), "" /* subtypeName */);
+        info.setIsAvailable(true);
+        // For compatibility with legacy code, return BLOCKED instead of DISCONNECTED when
+        // background data is restricted.
+        final NetworkCapabilities nc = new NetworkCapabilities();  // Metered.
+        final DetailedState state = isNetworkWithCapabilitiesBlocked(nc, uid, false)
+                ? DetailedState.BLOCKED
+                : DetailedState.DISCONNECTED;
+        info.setDetailedState(state, null /* reason */, null /* extraInfo */);
+        filterForLegacyLockdown(info);
+        return info;
+    }
+
+    private NetworkInfo getFilteredNetworkInfoForType(int networkType, int uid) {
+        if (!mLegacyTypeTracker.isTypeSupported(networkType)) {
+            return null;
+        }
+        final NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+        if (nai == null) {
+            return makeFakeNetworkInfo(networkType, uid);
+        }
+        return filterNetworkInfo(nai.networkInfo, networkType, nai.networkCapabilities, uid,
+                false);
+    }
+
+    @Override
+    public NetworkInfo getNetworkInfo(int networkType) {
+        enforceAccessPermission();
+        final int uid = mDeps.getCallingUid();
+        if (getVpnUnderlyingNetworks(uid) != null) {
+            // A VPN is active, so we may need to return one of its underlying networks. This
+            // information is not available in LegacyTypeTracker, so we have to get it from
+            // getNetworkAgentInfoForUid.
+            final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid);
+            if (nai == null) return null;
+            final NetworkInfo networkInfo = getFilteredNetworkInfo(nai, uid, false);
+            if (networkInfo.getType() == networkType) {
+                return networkInfo;
+            }
+        }
+        return getFilteredNetworkInfoForType(networkType, uid);
+    }
+
+    @Override
+    public NetworkInfo getNetworkInfoForUid(Network network, int uid, boolean ignoreBlocked) {
+        enforceAccessPermission();
+        final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+        if (nai == null) return null;
+        return getFilteredNetworkInfo(nai, uid, ignoreBlocked);
+    }
+
+    @Override
+    public NetworkInfo[] getAllNetworkInfo() {
+        enforceAccessPermission();
+        final ArrayList<NetworkInfo> result = new ArrayList<>();
+        for (int networkType = 0; networkType <= ConnectivityManager.MAX_NETWORK_TYPE;
+                networkType++) {
+            NetworkInfo info = getNetworkInfo(networkType);
+            if (info != null) {
+                result.add(info);
+            }
+        }
+        return result.toArray(new NetworkInfo[result.size()]);
+    }
+
+    @Override
+    public Network getNetworkForType(int networkType) {
+        enforceAccessPermission();
+        if (!mLegacyTypeTracker.isTypeSupported(networkType)) {
+            return null;
+        }
+        final NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+        if (nai == null) {
+            return null;
+        }
+        final int uid = mDeps.getCallingUid();
+        if (isNetworkWithCapabilitiesBlocked(nai.networkCapabilities, uid, false)) {
+            return null;
+        }
+        return nai.network;
+    }
+
+    @Override
+    public Network[] getAllNetworks() {
+        enforceAccessPermission();
+        synchronized (mNetworkForNetId) {
+            final Network[] result = new Network[mNetworkForNetId.size()];
+            for (int i = 0; i < mNetworkForNetId.size(); i++) {
+                result[i] = mNetworkForNetId.valueAt(i).network;
+            }
+            return result;
+        }
+    }
+
+    @Override
+    public NetworkCapabilities[] getDefaultNetworkCapabilitiesForUser(
+                int userId, String callingPackageName, @Nullable String callingAttributionTag) {
+        // The basic principle is: if an app's traffic could possibly go over a
+        // network, without the app doing anything multinetwork-specific,
+        // (hence, by "default"), then include that network's capabilities in
+        // the array.
+        //
+        // In the normal case, app traffic only goes over the system's default
+        // network connection, so that's the only network returned.
+        //
+        // With a VPN in force, some app traffic may go into the VPN, and thus
+        // over whatever underlying networks the VPN specifies, while other app
+        // traffic may go over the system default network (e.g.: a split-tunnel
+        // VPN, or an app disallowed by the VPN), so the set of networks
+        // returned includes the VPN's underlying networks and the system
+        // default.
+        enforceAccessPermission();
+
+        HashMap<Network, NetworkCapabilities> result = new HashMap<>();
+
+        for (final NetworkRequestInfo nri : mDefaultNetworkRequests) {
+            if (!nri.isBeingSatisfied()) {
+                continue;
+            }
+            final NetworkAgentInfo nai = nri.getSatisfier();
+            final NetworkCapabilities nc = getNetworkCapabilitiesInternal(nai);
+            if (null != nc
+                    && nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                    && !result.containsKey(nai.network)) {
+                result.put(
+                        nai.network,
+                        createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                                nc, false /* includeLocationSensitiveInfo */,
+                                getCallingPid(), mDeps.getCallingUid(), callingPackageName,
+                                callingAttributionTag));
+            }
+        }
+
+        // No need to check mLockdownEnabled. If it's true, getVpnUnderlyingNetworks returns null.
+        final Network[] networks = getVpnUnderlyingNetworks(mDeps.getCallingUid());
+        if (null != networks) {
+            for (final Network network : networks) {
+                final NetworkCapabilities nc = getNetworkCapabilitiesInternal(network);
+                if (null != nc) {
+                    result.put(
+                            network,
+                            createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                                    nc,
+                                    false /* includeLocationSensitiveInfo */,
+                                    getCallingPid(), mDeps.getCallingUid(), callingPackageName,
+                                    callingAttributionTag));
+                }
+            }
+        }
+
+        NetworkCapabilities[] out = new NetworkCapabilities[result.size()];
+        out = result.values().toArray(out);
+        return out;
+    }
+
+    @Override
+    public boolean isNetworkSupported(int networkType) {
+        enforceAccessPermission();
+        return mLegacyTypeTracker.isTypeSupported(networkType);
+    }
+
+    /**
+     * Return LinkProperties for the active (i.e., connected) default
+     * network interface for the calling uid.
+     * @return the ip properties for the active network, or {@code null} if
+     * none is active
+     */
+    @Override
+    public LinkProperties getActiveLinkProperties() {
+        enforceAccessPermission();
+        final int uid = mDeps.getCallingUid();
+        NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid);
+        if (nai == null) return null;
+        return linkPropertiesRestrictedForCallerPermissions(nai.linkProperties,
+                Binder.getCallingPid(), uid);
+    }
+
+    @Override
+    public LinkProperties getLinkPropertiesForType(int networkType) {
+        enforceAccessPermission();
+        NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+        final LinkProperties lp = getLinkProperties(nai);
+        if (lp == null) return null;
+        return linkPropertiesRestrictedForCallerPermissions(
+                lp, Binder.getCallingPid(), mDeps.getCallingUid());
+    }
+
+    // TODO - this should be ALL networks
+    @Override
+    public LinkProperties getLinkProperties(Network network) {
+        enforceAccessPermission();
+        final LinkProperties lp = getLinkProperties(getNetworkAgentInfoForNetwork(network));
+        if (lp == null) return null;
+        return linkPropertiesRestrictedForCallerPermissions(
+                lp, Binder.getCallingPid(), mDeps.getCallingUid());
+    }
+
+    @Nullable
+    private LinkProperties getLinkProperties(@Nullable NetworkAgentInfo nai) {
+        if (nai == null) {
+            return null;
+        }
+        synchronized (nai) {
+            return nai.linkProperties;
+        }
+    }
+
+    private NetworkCapabilities getNetworkCapabilitiesInternal(Network network) {
+        return getNetworkCapabilitiesInternal(getNetworkAgentInfoForNetwork(network));
+    }
+
+    private NetworkCapabilities getNetworkCapabilitiesInternal(NetworkAgentInfo nai) {
+        if (nai == null) return null;
+        synchronized (nai) {
+            return networkCapabilitiesRestrictedForCallerPermissions(
+                    nai.networkCapabilities, Binder.getCallingPid(), mDeps.getCallingUid());
+        }
+    }
+
+    @Override
+    public NetworkCapabilities getNetworkCapabilities(Network network, String callingPackageName,
+            @Nullable String callingAttributionTag) {
+        mAppOpsManager.checkPackage(mDeps.getCallingUid(), callingPackageName);
+        enforceAccessPermission();
+        return createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                getNetworkCapabilitiesInternal(network),
+                false /* includeLocationSensitiveInfo */,
+                getCallingPid(), mDeps.getCallingUid(), callingPackageName, callingAttributionTag);
+    }
+
+    @VisibleForTesting
+    NetworkCapabilities networkCapabilitiesRestrictedForCallerPermissions(
+            NetworkCapabilities nc, int callerPid, int callerUid) {
+        final NetworkCapabilities newNc = new NetworkCapabilities(nc);
+        if (!checkSettingsPermission(callerPid, callerUid)) {
+            newNc.setUids(null);
+            newNc.setSSID(null);
+        }
+        if (newNc.getNetworkSpecifier() != null) {
+            newNc.setNetworkSpecifier(newNc.getNetworkSpecifier().redact());
+        }
+        newNc.setAdministratorUids(new int[0]);
+        if (!checkAnyPermissionOf(
+                callerPid, callerUid, android.Manifest.permission.NETWORK_FACTORY)) {
+            newNc.setSubscriptionIds(Collections.emptySet());
+        }
+
+        return newNc;
+    }
+
+    /**
+     * Wrapper used to cache the permission check results performed for the corresponding
+     * app. This avoid performing multiple permission checks for different fields in
+     * NetworkCapabilities.
+     * Note: This wrapper does not support any sort of invalidation and thus must not be
+     * persistent or long-lived. It may only be used for the time necessary to
+     * compute the redactions required by one particular NetworkCallback or
+     * synchronous call.
+     */
+    private class RedactionPermissionChecker {
+        private final int mCallingPid;
+        private final int mCallingUid;
+        @NonNull private final String mCallingPackageName;
+        @Nullable private final String mCallingAttributionTag;
+
+        private Boolean mHasLocationPermission = null;
+        private Boolean mHasLocalMacAddressPermission = null;
+        private Boolean mHasSettingsPermission = null;
+
+        RedactionPermissionChecker(int callingPid, int callingUid,
+                @NonNull String callingPackageName, @Nullable String callingAttributionTag) {
+            mCallingPid = callingPid;
+            mCallingUid = callingUid;
+            mCallingPackageName = callingPackageName;
+            mCallingAttributionTag = callingAttributionTag;
+        }
+
+        private boolean hasLocationPermissionInternal() {
+            final long token = Binder.clearCallingIdentity();
+            try {
+                return mLocationPermissionChecker.checkLocationPermission(
+                        mCallingPackageName, mCallingAttributionTag, mCallingUid,
+                        null /* message */);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        /**
+         * Returns whether the app holds location permission or not (might return cached result
+         * if the permission was already checked before).
+         */
+        public boolean hasLocationPermission() {
+            if (mHasLocationPermission == null) {
+                // If there is no cached result, perform the check now.
+                mHasLocationPermission = hasLocationPermissionInternal();
+            }
+            return mHasLocationPermission;
+        }
+
+        /**
+         * Returns whether the app holds local mac address permission or not (might return cached
+         * result if the permission was already checked before).
+         */
+        public boolean hasLocalMacAddressPermission() {
+            if (mHasLocalMacAddressPermission == null) {
+                // If there is no cached result, perform the check now.
+                mHasLocalMacAddressPermission =
+                        checkLocalMacAddressPermission(mCallingPid, mCallingUid);
+            }
+            return mHasLocalMacAddressPermission;
+        }
+
+        /**
+         * Returns whether the app holds settings permission or not (might return cached
+         * result if the permission was already checked before).
+         */
+        public boolean hasSettingsPermission() {
+            if (mHasSettingsPermission == null) {
+                // If there is no cached result, perform the check now.
+                mHasSettingsPermission = checkSettingsPermission(mCallingPid, mCallingUid);
+            }
+            return mHasSettingsPermission;
+        }
+    }
+
+    private static boolean shouldRedact(@NetworkCapabilities.RedactionType long redactions,
+            @NetworkCapabilities.NetCapability long redaction) {
+        return (redactions & redaction) != 0;
+    }
+
+    /**
+     * Use the provided |applicableRedactions| to check the receiving app's
+     * permissions and clear/set the corresponding bit in the returned bitmask. The bitmask
+     * returned will be used to ensure the necessary redactions are performed by NetworkCapabilities
+     * before being sent to the corresponding app.
+     */
+    private @NetworkCapabilities.RedactionType long retrieveRequiredRedactions(
+            @NetworkCapabilities.RedactionType long applicableRedactions,
+            @NonNull RedactionPermissionChecker redactionPermissionChecker,
+            boolean includeLocationSensitiveInfo) {
+        long redactions = applicableRedactions;
+        if (shouldRedact(redactions, REDACT_FOR_ACCESS_FINE_LOCATION)) {
+            if (includeLocationSensitiveInfo
+                    && redactionPermissionChecker.hasLocationPermission()) {
+                redactions &= ~REDACT_FOR_ACCESS_FINE_LOCATION;
+            }
+        }
+        if (shouldRedact(redactions, REDACT_FOR_LOCAL_MAC_ADDRESS)) {
+            if (redactionPermissionChecker.hasLocalMacAddressPermission()) {
+                redactions &= ~REDACT_FOR_LOCAL_MAC_ADDRESS;
+            }
+        }
+        if (shouldRedact(redactions, REDACT_FOR_NETWORK_SETTINGS)) {
+            if (redactionPermissionChecker.hasSettingsPermission()) {
+                redactions &= ~REDACT_FOR_NETWORK_SETTINGS;
+            }
+        }
+        return redactions;
+    }
+
+    @VisibleForTesting
+    @Nullable
+    NetworkCapabilities createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+            @Nullable NetworkCapabilities nc, boolean includeLocationSensitiveInfo,
+            int callingPid, int callingUid, @NonNull String callingPkgName,
+            @Nullable String callingAttributionTag) {
+        if (nc == null) {
+            return null;
+        }
+        // Avoid doing location permission check if the transport info has no location sensitive
+        // data.
+        final RedactionPermissionChecker redactionPermissionChecker =
+                new RedactionPermissionChecker(callingPid, callingUid, callingPkgName,
+                        callingAttributionTag);
+        final long redactions = retrieveRequiredRedactions(
+                nc.getApplicableRedactions(), redactionPermissionChecker,
+                includeLocationSensitiveInfo);
+        final NetworkCapabilities newNc = new NetworkCapabilities(nc, redactions);
+        // Reset owner uid if not destined for the owner app.
+        if (callingUid != nc.getOwnerUid()) {
+            newNc.setOwnerUid(INVALID_UID);
+            return newNc;
+        }
+        // Allow VPNs to see ownership of their own VPN networks - not location sensitive.
+        if (nc.hasTransport(TRANSPORT_VPN)) {
+            // Owner UIDs already checked above. No need to re-check.
+            return newNc;
+        }
+        // If the calling does not want location sensitive data & target SDK >= S, then mask info.
+        // Else include the owner UID iff the calling has location permission to provide backwards
+        // compatibility for older apps.
+        if (!includeLocationSensitiveInfo
+                && isTargetSdkAtleast(
+                        Build.VERSION_CODES.S, callingUid, callingPkgName)) {
+            newNc.setOwnerUid(INVALID_UID);
+            return newNc;
+        }
+        // Reset owner uid if the app has no location permission.
+        if (!redactionPermissionChecker.hasLocationPermission()) {
+            newNc.setOwnerUid(INVALID_UID);
+        }
+        return newNc;
+    }
+
+    private LinkProperties linkPropertiesRestrictedForCallerPermissions(
+            LinkProperties lp, int callerPid, int callerUid) {
+        if (lp == null) return new LinkProperties();
+
+        // Only do a permission check if sanitization is needed, to avoid unnecessary binder calls.
+        final boolean needsSanitization =
+                (lp.getCaptivePortalApiUrl() != null || lp.getCaptivePortalData() != null);
+        if (!needsSanitization) {
+            return new LinkProperties(lp);
+        }
+
+        if (checkSettingsPermission(callerPid, callerUid)) {
+            return new LinkProperties(lp, true /* parcelSensitiveFields */);
+        }
+
+        final LinkProperties newLp = new LinkProperties(lp);
+        // Sensitive fields would not be parceled anyway, but sanitize for consistency before the
+        // object gets parceled.
+        newLp.setCaptivePortalApiUrl(null);
+        newLp.setCaptivePortalData(null);
+        return newLp;
+    }
+
+    private void restrictRequestUidsForCallerAndSetRequestorInfo(NetworkCapabilities nc,
+            int callerUid, String callerPackageName) {
+        if (!checkSettingsPermission()) {
+            // There is no need to track the effective UID of the request here. If the caller lacks
+            // the settings permission, the effective UID is the same as the calling ID.
+            nc.setSingleUid(callerUid);
+        }
+        nc.setRequestorUidAndPackageName(callerUid, callerPackageName);
+        nc.setAdministratorUids(new int[0]);
+
+        // Clear owner UID; this can never come from an app.
+        nc.setOwnerUid(INVALID_UID);
+    }
+
+    private void restrictBackgroundRequestForCaller(NetworkCapabilities nc) {
+        if (!mPermissionMonitor.hasUseBackgroundNetworksPermission(mDeps.getCallingUid())) {
+            nc.addCapability(NET_CAPABILITY_FOREGROUND);
+        }
+    }
+
+    @Override
+    public @RestrictBackgroundStatus int getRestrictBackgroundStatusByCaller() {
+        enforceAccessPermission();
+        final int callerUid = Binder.getCallingUid();
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return mPolicyManager.getRestrictBackgroundStatus(callerUid);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    // TODO: Consider delete this function or turn it into a no-op method.
+    @Override
+    public NetworkState[] getAllNetworkState() {
+        // This contains IMSI details, so make sure the caller is privileged.
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+
+        final ArrayList<NetworkState> result = new ArrayList<>();
+        for (NetworkStateSnapshot snapshot : getAllNetworkStateSnapshots()) {
+            // NetworkStateSnapshot doesn't contain NetworkInfo, so need to fetch it from the
+            // NetworkAgentInfo.
+            final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork());
+            if (nai != null && nai.networkInfo.isConnected()) {
+                result.add(new NetworkState(new NetworkInfo(nai.networkInfo),
+                        snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),
+                        snapshot.getNetwork(), snapshot.getSubscriberId()));
+            }
+        }
+        return result.toArray(new NetworkState[result.size()]);
+    }
+
+    @Override
+    @NonNull
+    public List<NetworkStateSnapshot> getAllNetworkStateSnapshots() {
+        // This contains IMSI details, so make sure the caller is privileged.
+        enforceNetworkStackOrSettingsPermission();
+
+        final ArrayList<NetworkStateSnapshot> result = new ArrayList<>();
+        for (Network network : getAllNetworks()) {
+            final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+            // TODO: Consider include SUSPENDED networks, which should be considered as
+            //  temporary shortage of connectivity of a connected network.
+            if (nai != null && nai.networkInfo.isConnected()) {
+                // TODO (b/73321673) : NetworkStateSnapshot contains a copy of the
+                // NetworkCapabilities, which may contain UIDs of apps to which the
+                // network applies. Should the UIDs be cleared so as not to leak or
+                // interfere ?
+                result.add(nai.getNetworkStateSnapshot());
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public boolean isActiveNetworkMetered() {
+        enforceAccessPermission();
+
+        final NetworkCapabilities caps = getNetworkCapabilitiesInternal(getActiveNetwork());
+        if (caps != null) {
+            return !caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+        } else {
+            // Always return the most conservative value
+            return true;
+        }
+    }
+
+    /**
+     * Ensures that the system cannot call a particular method.
+     */
+    private boolean disallowedBecauseSystemCaller() {
+        // TODO: start throwing a SecurityException when GnssLocationProvider stops calling
+        // requestRouteToHost. In Q, GnssLocationProvider is changed to not call requestRouteToHost
+        // for devices launched with Q and above. However, existing devices upgrading to Q and
+        // above must continued to be supported for few more releases.
+        if (isSystem(mDeps.getCallingUid()) && SystemProperties.getInt(
+                "ro.product.first_api_level", 0) > Build.VERSION_CODES.P) {
+            log("This method exists only for app backwards compatibility"
+                    + " and must not be called by system services.");
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Ensure that a network route exists to deliver traffic to the specified
+     * host via the specified network interface.
+     * @param networkType the type of the network over which traffic to the
+     * specified host is to be routed
+     * @param hostAddress the IP address of the host to which the route is
+     * desired
+     * @return {@code true} on success, {@code false} on failure
+     */
+    @Override
+    public boolean requestRouteToHostAddress(int networkType, byte[] hostAddress,
+            String callingPackageName, String callingAttributionTag) {
+        if (disallowedBecauseSystemCaller()) {
+            return false;
+        }
+        enforceChangePermission(callingPackageName, callingAttributionTag);
+        if (mProtectedNetworks.contains(networkType)) {
+            enforceConnectivityRestrictedNetworksPermission();
+        }
+
+        InetAddress addr;
+        try {
+            addr = InetAddress.getByAddress(hostAddress);
+        } catch (UnknownHostException e) {
+            if (DBG) log("requestRouteToHostAddress got " + e.toString());
+            return false;
+        }
+
+        if (!ConnectivityManager.isNetworkTypeValid(networkType)) {
+            if (DBG) log("requestRouteToHostAddress on invalid network: " + networkType);
+            return false;
+        }
+
+        NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+        if (nai == null) {
+            if (mLegacyTypeTracker.isTypeSupported(networkType) == false) {
+                if (DBG) log("requestRouteToHostAddress on unsupported network: " + networkType);
+            } else {
+                if (DBG) log("requestRouteToHostAddress on down network: " + networkType);
+            }
+            return false;
+        }
+
+        DetailedState netState;
+        synchronized (nai) {
+            netState = nai.networkInfo.getDetailedState();
+        }
+
+        if (netState != DetailedState.CONNECTED && netState != DetailedState.CAPTIVE_PORTAL_CHECK) {
+            if (VDBG) {
+                log("requestRouteToHostAddress on down network "
+                        + "(" + networkType + ") - dropped"
+                        + " netState=" + netState);
+            }
+            return false;
+        }
+
+        final int uid = mDeps.getCallingUid();
+        final long token = Binder.clearCallingIdentity();
+        try {
+            LinkProperties lp;
+            int netId;
+            synchronized (nai) {
+                lp = nai.linkProperties;
+                netId = nai.network.getNetId();
+            }
+            boolean ok = addLegacyRouteToHost(lp, addr, netId, uid);
+            if (DBG) {
+                log("requestRouteToHostAddress " + addr + nai.toShortString() + " ok=" + ok);
+            }
+            return ok;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    private boolean addLegacyRouteToHost(LinkProperties lp, InetAddress addr, int netId, int uid) {
+        RouteInfo bestRoute = RouteInfo.selectBestRoute(lp.getAllRoutes(), addr);
+        if (bestRoute == null) {
+            bestRoute = RouteInfo.makeHostRoute(addr, lp.getInterfaceName());
+        } else {
+            String iface = bestRoute.getInterface();
+            if (bestRoute.getGateway().equals(addr)) {
+                // if there is no better route, add the implied hostroute for our gateway
+                bestRoute = RouteInfo.makeHostRoute(addr, iface);
+            } else {
+                // if we will connect to this through another route, add a direct route
+                // to it's gateway
+                bestRoute = RouteInfo.makeHostRoute(addr, bestRoute.getGateway(), iface);
+            }
+        }
+        if (DBG) log("Adding legacy route " + bestRoute +
+                " for UID/PID " + uid + "/" + Binder.getCallingPid());
+
+        final String dst = bestRoute.getDestinationLinkAddress().toString();
+        final String nextHop = bestRoute.hasGateway()
+                ? bestRoute.getGateway().getHostAddress() : "";
+        try {
+            mNetd.networkAddLegacyRoute(netId, bestRoute.getInterface(), dst, nextHop , uid);
+        } catch (RemoteException | ServiceSpecificException e) {
+            if (DBG) loge("Exception trying to add a route: " + e);
+            return false;
+        }
+        return true;
+    }
+
+    class DnsResolverUnsolicitedEventCallback extends
+            IDnsResolverUnsolicitedEventListener.Stub {
+        @Override
+        public void onPrivateDnsValidationEvent(final PrivateDnsValidationEventParcel event) {
+            try {
+                mHandler.sendMessage(mHandler.obtainMessage(
+                        EVENT_PRIVATE_DNS_VALIDATION_UPDATE,
+                        new PrivateDnsValidationUpdate(event.netId,
+                                InetAddresses.parseNumericAddress(event.ipAddress),
+                                event.hostname, event.validation)));
+            } catch (IllegalArgumentException e) {
+                loge("Error parsing ip address in validation event");
+            }
+        }
+
+        @Override
+        public void onDnsHealthEvent(final DnsHealthEventParcel event) {
+            NetworkAgentInfo nai = getNetworkAgentInfoForNetId(event.netId);
+            // Netd event only allow registrants from system. Each NetworkMonitor thread is under
+            // the caller thread of registerNetworkAgent. Thus, it's not allowed to register netd
+            // event callback for certain nai. e.g. cellular. Register here to pass to
+            // NetworkMonitor instead.
+            // TODO: Move the Dns Event to NetworkMonitor. NetdEventListenerService only allow one
+            // callback from each caller type. Need to re-factor NetdEventListenerService to allow
+            // multiple NetworkMonitor registrants.
+            if (nai != null && nai.satisfies(mDefaultRequest.mRequests.get(0))) {
+                nai.networkMonitor().notifyDnsResponse(event.healthResult);
+            }
+        }
+
+        @Override
+        public void onNat64PrefixEvent(final Nat64PrefixEventParcel event) {
+            mHandler.post(() -> handleNat64PrefixEvent(event.netId, event.prefixOperation,
+                    event.prefixAddress, event.prefixLength));
+        }
+
+        @Override
+        public int getInterfaceVersion() {
+            return this.VERSION;
+        }
+
+        @Override
+        public String getInterfaceHash() {
+            return this.HASH;
+        }
+    }
+
+    @VisibleForTesting
+    protected final DnsResolverUnsolicitedEventCallback mResolverUnsolEventCallback =
+            new DnsResolverUnsolicitedEventCallback();
+
+    private void registerDnsResolverUnsolicitedEventListener() {
+        try {
+            mDnsResolver.registerUnsolicitedEventListener(mResolverUnsolEventCallback);
+        } catch (Exception e) {
+            loge("Error registering DnsResolver unsolicited event callback: " + e);
+        }
+    }
+
+    private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {
+        @Override
+        public void onUidBlockedReasonChanged(int uid, @BlockedReason int blockedReasons) {
+            mHandler.sendMessage(mHandler.obtainMessage(EVENT_UID_BLOCKED_REASON_CHANGED,
+                    uid, blockedReasons));
+        }
+    };
+
+    private void handleUidBlockedReasonChanged(int uid, @BlockedReason int blockedReasons) {
+        maybeNotifyNetworkBlockedForNewState(uid, blockedReasons);
+        setUidBlockedReasons(uid, blockedReasons);
+    }
+
+    private boolean checkAnyPermissionOf(String... permissions) {
+        for (String permission : permissions) {
+            if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean checkAnyPermissionOf(int pid, int uid, String... permissions) {
+        for (String permission : permissions) {
+            if (mContext.checkPermission(permission, pid, uid) == 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) + ".");
+        }
+    }
+
+    private void enforceInternetPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.INTERNET,
+                "ConnectivityService");
+    }
+
+    private void enforceAccessPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.ACCESS_NETWORK_STATE,
+                "ConnectivityService");
+    }
+
+    /**
+     * Performs a strict and comprehensive check of whether a calling package is allowed to
+     * change the state of network, as the condition differs for pre-M, M+, and
+     * privileged/preinstalled apps. The caller is expected to have either the
+     * CHANGE_NETWORK_STATE or the WRITE_SETTINGS permission declared. Either of these
+     * permissions allow changing network state; WRITE_SETTINGS is a runtime permission and
+     * can be revoked, but (except in M, excluding M MRs), CHANGE_NETWORK_STATE is a normal
+     * permission and cannot be revoked. See http://b/23597341
+     *
+     * Note: if the check succeeds because the application holds WRITE_SETTINGS, the operation
+     * of this app will be updated to the current time.
+     */
+    private void enforceChangePermission(String callingPkg, String callingAttributionTag) {
+        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.CHANGE_NETWORK_STATE)
+                == PackageManager.PERMISSION_GRANTED) {
+            return;
+        }
+
+        if (callingPkg == null) {
+            throw new SecurityException("Calling package name is null.");
+        }
+
+        final AppOpsManager appOpsMgr = mContext.getSystemService(AppOpsManager.class);
+        final int uid = mDeps.getCallingUid();
+        final int mode = appOpsMgr.noteOpNoThrow(AppOpsManager.OPSTR_WRITE_SETTINGS, uid,
+                callingPkg, callingAttributionTag, null /* message */);
+
+        if (mode == AppOpsManager.MODE_ALLOWED) {
+            return;
+        }
+
+        if ((mode == AppOpsManager.MODE_DEFAULT) && (mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.WRITE_SETTINGS) == PackageManager.PERMISSION_GRANTED)) {
+            return;
+        }
+
+        throw new SecurityException(callingPkg + " was not granted either of these permissions:"
+                + android.Manifest.permission.CHANGE_NETWORK_STATE + ","
+                + android.Manifest.permission.WRITE_SETTINGS + ".");
+    }
+
+    private void enforceSettingsPermission() {
+        enforceAnyPermissionOf(
+                android.Manifest.permission.NETWORK_SETTINGS,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+    }
+
+    private void enforceNetworkFactoryPermission() {
+        enforceAnyPermissionOf(
+                android.Manifest.permission.NETWORK_FACTORY,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+    }
+
+    private void enforceNetworkFactoryOrSettingsPermission() {
+        enforceAnyPermissionOf(
+                android.Manifest.permission.NETWORK_SETTINGS,
+                android.Manifest.permission.NETWORK_FACTORY,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+    }
+
+    private void enforceNetworkFactoryOrTestNetworksPermission() {
+        enforceAnyPermissionOf(
+                android.Manifest.permission.MANAGE_TEST_NETWORKS,
+                android.Manifest.permission.NETWORK_FACTORY,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+    }
+
+    private boolean checkSettingsPermission() {
+        return checkAnyPermissionOf(
+                android.Manifest.permission.NETWORK_SETTINGS,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+    }
+
+    private boolean checkSettingsPermission(int pid, int uid) {
+        return PERMISSION_GRANTED == mContext.checkPermission(
+                android.Manifest.permission.NETWORK_SETTINGS, pid, uid)
+                || PERMISSION_GRANTED == mContext.checkPermission(
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, pid, uid);
+    }
+
+    private void enforceNetworkStackOrSettingsPermission() {
+        enforceAnyPermissionOf(
+                android.Manifest.permission.NETWORK_SETTINGS,
+                android.Manifest.permission.NETWORK_STACK,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+    }
+
+    private void enforceNetworkStackSettingsOrSetup() {
+        enforceAnyPermissionOf(
+                android.Manifest.permission.NETWORK_SETTINGS,
+                android.Manifest.permission.NETWORK_SETUP_WIZARD,
+                android.Manifest.permission.NETWORK_STACK,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+    }
+
+    private void enforceAirplaneModePermission() {
+        enforceAnyPermissionOf(
+                android.Manifest.permission.NETWORK_AIRPLANE_MODE,
+                android.Manifest.permission.NETWORK_SETTINGS,
+                android.Manifest.permission.NETWORK_SETUP_WIZARD,
+                android.Manifest.permission.NETWORK_STACK,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+    }
+
+    private void enforceOemNetworkPreferencesPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE,
+                "ConnectivityService");
+    }
+
+    private void enforceManageTestNetworksPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MANAGE_TEST_NETWORKS,
+                "ConnectivityService");
+    }
+
+    private boolean checkNetworkStackPermission() {
+        return checkAnyPermissionOf(
+                android.Manifest.permission.NETWORK_STACK,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+    }
+
+    private boolean checkNetworkStackPermission(int pid, int uid) {
+        return checkAnyPermissionOf(pid, uid,
+                android.Manifest.permission.NETWORK_STACK,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+    }
+
+    private boolean checkNetworkSignalStrengthWakeupPermission(int pid, int uid) {
+        return checkAnyPermissionOf(pid, uid,
+                android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                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 void enforceKeepalivePermission() {
+        mContext.enforceCallingOrSelfPermission(KeepaliveTracker.PERMISSION, "ConnectivityService");
+    }
+
+    private boolean checkLocalMacAddressPermission(int pid, int uid) {
+        return PERMISSION_GRANTED == mContext.checkPermission(
+                Manifest.permission.LOCAL_MAC_ADDRESS, pid, uid);
+    }
+
+    private void sendConnectedBroadcast(NetworkInfo info) {
+        sendGeneralBroadcast(info, CONNECTIVITY_ACTION);
+    }
+
+    private void sendInetConditionBroadcast(NetworkInfo info) {
+        sendGeneralBroadcast(info, ConnectivityManager.INET_CONDITION_ACTION);
+    }
+
+    private Intent makeGeneralIntent(NetworkInfo info, String bcastType) {
+        Intent intent = new Intent(bcastType);
+        intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, new NetworkInfo(info));
+        intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType());
+        if (info.isFailover()) {
+            intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true);
+            info.setFailover(false);
+        }
+        if (info.getReason() != null) {
+            intent.putExtra(ConnectivityManager.EXTRA_REASON, info.getReason());
+        }
+        if (info.getExtraInfo() != null) {
+            intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO,
+                    info.getExtraInfo());
+        }
+        intent.putExtra(ConnectivityManager.EXTRA_INET_CONDITION, mDefaultInetConditionPublished);
+        return intent;
+    }
+
+    private void sendGeneralBroadcast(NetworkInfo info, String bcastType) {
+        sendStickyBroadcast(makeGeneralIntent(info, bcastType));
+    }
+
+    private void sendStickyBroadcast(Intent intent) {
+        synchronized (this) {
+            if (!mSystemReady
+                    && intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+                mInitialBroadcast = new Intent(intent);
+            }
+            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+            if (VDBG) {
+                log("sendStickyBroadcast: action=" + intent.getAction());
+            }
+
+            Bundle options = null;
+            final long ident = Binder.clearCallingIdentity();
+            if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
+                final NetworkInfo ni = intent.getParcelableExtra(
+                        ConnectivityManager.EXTRA_NETWORK_INFO);
+                final BroadcastOptions opts = BroadcastOptions.makeBasic();
+                opts.setMaxManifestReceiverApiLevel(Build.VERSION_CODES.M);
+                options = opts.toBundle();
+                intent.addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
+            }
+            try {
+                mUserAllContext.sendStickyBroadcast(intent, options);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+    }
+
+    /**
+     * Called by SystemServer through ConnectivityManager when the system is ready.
+     */
+    @Override
+    public void systemReady() {
+        if (mDeps.getCallingUid() != Process.SYSTEM_UID) {
+            throw new SecurityException("Calling Uid is not system uid.");
+        }
+        systemReadyInternal();
+    }
+
+    /**
+     * Called when ConnectivityService can initialize remaining components.
+     */
+    @VisibleForTesting
+    public void systemReadyInternal() {
+        // Since mApps in PermissionMonitor needs to be populated first to ensure that
+        // listening network request which is sent by MultipathPolicyTracker won't be added
+        // NET_CAPABILITY_FOREGROUND capability. Thus, MultipathPolicyTracker.start() must
+        // be called after PermissionMonitor#startMonitoring().
+        // Calling PermissionMonitor#startMonitoring() in systemReadyInternal() and the
+        // MultipathPolicyTracker.start() is called in NetworkPolicyManagerService#systemReady()
+        // to ensure the tracking will be initialized correctly.
+        mPermissionMonitor.startMonitoring();
+        mProxyTracker.loadGlobalProxy();
+        registerDnsResolverUnsolicitedEventListener();
+
+        synchronized (this) {
+            mSystemReady = true;
+            if (mInitialBroadcast != null) {
+                mContext.sendStickyBroadcastAsUser(mInitialBroadcast, UserHandle.ALL);
+                mInitialBroadcast = null;
+            }
+        }
+
+        // Create network requests for always-on networks.
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_CONFIGURE_ALWAYS_ON_NETWORKS));
+
+        // Update mobile data preference if necessary.
+        // Note that empty uid list can be skip here only because no uid rules applied before system
+        // ready. Normally, the empty uid list means to clear the uids rules on netd.
+        if (!ConnectivitySettingsManager.getMobileDataPreferredUids(mContext).isEmpty()) {
+            updateMobileDataPreferredUids();
+        }
+    }
+
+    /**
+     * Start listening for default data network activity state changes.
+     */
+    @Override
+    public void registerNetworkActivityListener(@NonNull INetworkActivityListener l) {
+        mNetworkActivityTracker.registerNetworkActivityListener(l);
+    }
+
+    /**
+     * Stop listening for default data network activity state changes.
+     */
+    @Override
+    public void unregisterNetworkActivityListener(@NonNull INetworkActivityListener l) {
+        mNetworkActivityTracker.unregisterNetworkActivityListener(l);
+    }
+
+    /**
+     * Check whether the default network radio is currently active.
+     */
+    @Override
+    public boolean isDefaultNetworkActive() {
+        return mNetworkActivityTracker.isDefaultNetworkActive();
+    }
+
+    /**
+     * Reads the network specific MTU size from resources.
+     * and set it on it's iface.
+     */
+    private void updateMtu(LinkProperties newLp, LinkProperties oldLp) {
+        final String iface = newLp.getInterfaceName();
+        final int mtu = newLp.getMtu();
+        if (oldLp == null && mtu == 0) {
+            // Silently ignore unset MTU value.
+            return;
+        }
+        if (oldLp != null && newLp.isIdenticalMtu(oldLp)) {
+            if (VDBG) log("identical MTU - not setting");
+            return;
+        }
+        if (!LinkProperties.isValidMtu(mtu, newLp.hasGlobalIpv6Address())) {
+            if (mtu != 0) loge("Unexpected mtu value: " + mtu + ", " + iface);
+            return;
+        }
+
+        // Cannot set MTU without interface name
+        if (TextUtils.isEmpty(iface)) {
+            loge("Setting MTU size with null iface.");
+            return;
+        }
+
+        try {
+            if (VDBG || DDBG) log("Setting MTU size: " + iface + ", " + mtu);
+            mNetd.interfaceSetMtu(iface, mtu);
+        } catch (RemoteException | ServiceSpecificException e) {
+            loge("exception in interfaceSetMtu()" + e);
+        }
+    }
+
+    @VisibleForTesting
+    protected static final String DEFAULT_TCP_BUFFER_SIZES = "4096,87380,110208,4096,16384,110208";
+
+    private void updateTcpBufferSizes(String tcpBufferSizes) {
+        String[] values = null;
+        if (tcpBufferSizes != null) {
+            values = tcpBufferSizes.split(",");
+        }
+
+        if (values == null || values.length != 6) {
+            if (DBG) log("Invalid tcpBufferSizes string: " + tcpBufferSizes +", using defaults");
+            tcpBufferSizes = DEFAULT_TCP_BUFFER_SIZES;
+            values = tcpBufferSizes.split(",");
+        }
+
+        if (tcpBufferSizes.equals(mCurrentTcpBufferSizes)) return;
+
+        try {
+            if (VDBG || DDBG) log("Setting tx/rx TCP buffers to " + tcpBufferSizes);
+
+            String rmemValues = String.join(" ", values[0], values[1], values[2]);
+            String wmemValues = String.join(" ", values[3], values[4], values[5]);
+            mNetd.setTcpRWmemorySize(rmemValues, wmemValues);
+            mCurrentTcpBufferSizes = tcpBufferSizes;
+        } catch (RemoteException | ServiceSpecificException e) {
+            loge("Can't set TCP buffer sizes:" + e);
+        }
+    }
+
+    @Override
+    public int getRestoreDefaultNetworkDelay(int networkType) {
+        String restoreDefaultNetworkDelayStr = mSystemProperties.get(
+                NETWORK_RESTORE_DELAY_PROP_NAME);
+        if(restoreDefaultNetworkDelayStr != null &&
+                restoreDefaultNetworkDelayStr.length() != 0) {
+            try {
+                return Integer.parseInt(restoreDefaultNetworkDelayStr);
+            } catch (NumberFormatException e) {
+            }
+        }
+        // if the system property isn't set, use the value for the apn type
+        int ret = RESTORE_DEFAULT_NETWORK_DELAY;
+
+        if (mLegacyTypeTracker.isTypeSupported(networkType)) {
+            ret = mLegacyTypeTracker.getRestoreTimerForType(networkType);
+        }
+        return ret;
+    }
+
+    private void dumpNetworkDiagnostics(IndentingPrintWriter pw) {
+        final List<NetworkDiagnostics> netDiags = new ArrayList<NetworkDiagnostics>();
+        final long DIAG_TIME_MS = 5000;
+        for (NetworkAgentInfo nai : networksSortedById()) {
+            PrivateDnsConfig privateDnsCfg = mDnsManager.getPrivateDnsConfig(nai.network);
+            // Start gathering diagnostic information.
+            netDiags.add(new NetworkDiagnostics(
+                    nai.network,
+                    new LinkProperties(nai.linkProperties),  // Must be a copy.
+                    privateDnsCfg,
+                    DIAG_TIME_MS));
+        }
+
+        for (NetworkDiagnostics netDiag : netDiags) {
+            pw.println();
+            netDiag.waitForMeasurements();
+            netDiag.dump(pw);
+        }
+    }
+
+    @Override
+    protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
+            @Nullable String[] args) {
+        if (!checkDumpPermission(mContext, TAG, writer)) return;
+
+        mPriorityDumper.dump(fd, writer, args);
+    }
+
+    private boolean checkDumpPermission(Context context, String tag, PrintWriter pw) {
+        if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+                != PackageManager.PERMISSION_GRANTED) {
+            pw.println("Permission Denial: can't dump " + tag + " from from pid="
+                    + Binder.getCallingPid() + ", uid=" + mDeps.getCallingUid()
+                    + " due to missing android.permission.DUMP permission");
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    private void doDump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
+
+        if (CollectionUtils.contains(args, DIAG_ARG)) {
+            dumpNetworkDiagnostics(pw);
+            return;
+        } else if (CollectionUtils.contains(args, NETWORK_ARG)) {
+            dumpNetworks(pw);
+            return;
+        } else if (CollectionUtils.contains(args, REQUEST_ARG)) {
+            dumpNetworkRequests(pw);
+            return;
+        }
+
+        pw.print("NetworkProviders for:");
+        for (NetworkProviderInfo npi : mNetworkProviderInfos.values()) {
+            pw.print(" " + npi.name);
+        }
+        pw.println();
+        pw.println();
+
+        final NetworkAgentInfo defaultNai = getDefaultNetwork();
+        pw.print("Active default network: ");
+        if (defaultNai == null) {
+            pw.println("none");
+        } else {
+            pw.println(defaultNai.network.getNetId());
+        }
+        pw.println();
+
+        pw.print("Current per-app default networks: ");
+        pw.increaseIndent();
+        dumpPerAppNetworkPreferences(pw);
+        pw.decreaseIndent();
+        pw.println();
+
+        pw.println("Current Networks:");
+        pw.increaseIndent();
+        dumpNetworks(pw);
+        pw.decreaseIndent();
+        pw.println();
+
+        pw.println("Status for known UIDs:");
+        pw.increaseIndent();
+        final int size = mUidBlockedReasons.size();
+        for (int i = 0; i < size; i++) {
+            // Don't crash if the array is modified while dumping in bugreports.
+            try {
+                final int uid = mUidBlockedReasons.keyAt(i);
+                final int blockedReasons = mUidBlockedReasons.valueAt(i);
+                pw.println("UID=" + uid + " blockedReasons="
+                        + Integer.toHexString(blockedReasons));
+            } catch (ArrayIndexOutOfBoundsException e) {
+                pw.println("  ArrayIndexOutOfBoundsException");
+            } catch (ConcurrentModificationException e) {
+                pw.println("  ConcurrentModificationException");
+            }
+        }
+        pw.println();
+        pw.decreaseIndent();
+
+        pw.println("Network Requests:");
+        pw.increaseIndent();
+        dumpNetworkRequests(pw);
+        pw.decreaseIndent();
+        pw.println();
+
+        mLegacyTypeTracker.dump(pw);
+
+        pw.println();
+        mKeepaliveTracker.dump(pw);
+
+        pw.println();
+        dumpAvoidBadWifiSettings(pw);
+
+        pw.println();
+
+        if (!CollectionUtils.contains(args, SHORT_ARG)) {
+            pw.println();
+            pw.println("mNetworkRequestInfoLogs (most recent first):");
+            pw.increaseIndent();
+            mNetworkRequestInfoLogs.reverseDump(pw);
+            pw.decreaseIndent();
+
+            pw.println();
+            pw.println("mNetworkInfoBlockingLogs (most recent first):");
+            pw.increaseIndent();
+            mNetworkInfoBlockingLogs.reverseDump(pw);
+            pw.decreaseIndent();
+
+            pw.println();
+            pw.println("NetTransition WakeLock activity (most recent first):");
+            pw.increaseIndent();
+            pw.println("total acquisitions: " + mTotalWakelockAcquisitions);
+            pw.println("total releases: " + mTotalWakelockReleases);
+            pw.println("cumulative duration: " + (mTotalWakelockDurationMs / 1000) + "s");
+            pw.println("longest duration: " + (mMaxWakelockDurationMs / 1000) + "s");
+            if (mTotalWakelockAcquisitions > mTotalWakelockReleases) {
+                long duration = SystemClock.elapsedRealtime() - mLastWakeLockAcquireTimestamp;
+                pw.println("currently holding WakeLock for: " + (duration / 1000) + "s");
+            }
+            mWakelockLogs.reverseDump(pw);
+
+            pw.println();
+            pw.println("bandwidth update requests (by uid):");
+            pw.increaseIndent();
+            synchronized (mBandwidthRequests) {
+                for (int i = 0; i < mBandwidthRequests.size(); i++) {
+                    pw.println("[" + mBandwidthRequests.keyAt(i)
+                            + "]: " + mBandwidthRequests.valueAt(i));
+                }
+            }
+            pw.decreaseIndent();
+            pw.decreaseIndent();
+
+            pw.println();
+            pw.println("mOemNetworkPreferencesLogs (most recent first):");
+            pw.increaseIndent();
+            mOemNetworkPreferencesLogs.reverseDump(pw);
+            pw.decreaseIndent();
+        }
+
+        pw.println();
+
+        pw.println();
+        pw.println("Permission Monitor:");
+        pw.increaseIndent();
+        mPermissionMonitor.dump(pw);
+        pw.decreaseIndent();
+
+        pw.println();
+        pw.println("Legacy network activity:");
+        pw.increaseIndent();
+        mNetworkActivityTracker.dump(pw);
+        pw.decreaseIndent();
+    }
+
+    private void dumpNetworks(IndentingPrintWriter pw) {
+        for (NetworkAgentInfo nai : networksSortedById()) {
+            pw.println(nai.toString());
+            pw.increaseIndent();
+            pw.println(String.format(
+                    "Requests: REQUEST:%d LISTEN:%d BACKGROUND_REQUEST:%d total:%d",
+                    nai.numForegroundNetworkRequests(),
+                    nai.numNetworkRequests() - nai.numRequestNetworkRequests(),
+                    nai.numBackgroundNetworkRequests(),
+                    nai.numNetworkRequests()));
+            pw.increaseIndent();
+            for (int i = 0; i < nai.numNetworkRequests(); i++) {
+                pw.println(nai.requestAt(i).toString());
+            }
+            pw.decreaseIndent();
+            pw.println("Inactivity Timers:");
+            pw.increaseIndent();
+            nai.dumpInactivityTimers(pw);
+            pw.decreaseIndent();
+            pw.decreaseIndent();
+        }
+    }
+
+    private void dumpPerAppNetworkPreferences(IndentingPrintWriter pw) {
+        pw.println("Per-App Network Preference:");
+        pw.increaseIndent();
+        if (0 == mOemNetworkPreferences.getNetworkPreferences().size()) {
+            pw.println("none");
+        } else {
+            pw.println(mOemNetworkPreferences.toString());
+        }
+        pw.decreaseIndent();
+
+        for (final NetworkRequestInfo defaultRequest : mDefaultNetworkRequests) {
+            if (mDefaultRequest == defaultRequest) {
+                continue;
+            }
+
+            final boolean isActive = null != defaultRequest.getSatisfier();
+            pw.println("Is per-app network active:");
+            pw.increaseIndent();
+            pw.println(isActive);
+            if (isActive) {
+                pw.println("Active network: " + defaultRequest.getSatisfier().network.netId);
+            }
+            pw.println("Tracked UIDs:");
+            pw.increaseIndent();
+            if (0 == defaultRequest.mRequests.size()) {
+                pw.println("none, this should never occur.");
+            } else {
+                pw.println(defaultRequest.mRequests.get(0).networkCapabilities.getUidRanges());
+            }
+            pw.decreaseIndent();
+            pw.decreaseIndent();
+        }
+    }
+
+    private void dumpNetworkRequests(IndentingPrintWriter pw) {
+        for (NetworkRequestInfo nri : requestsSortedById()) {
+            pw.println(nri.toString());
+        }
+    }
+
+    /**
+     * Return an array of all current NetworkAgentInfos sorted by network id.
+     */
+    private NetworkAgentInfo[] networksSortedById() {
+        NetworkAgentInfo[] networks = new NetworkAgentInfo[0];
+        networks = mNetworkAgentInfos.toArray(networks);
+        Arrays.sort(networks, Comparator.comparingInt(nai -> nai.network.getNetId()));
+        return networks;
+    }
+
+    /**
+     * Return an array of all current NetworkRequest sorted by request id.
+     */
+    @VisibleForTesting
+    NetworkRequestInfo[] requestsSortedById() {
+        NetworkRequestInfo[] requests = new NetworkRequestInfo[0];
+        requests = getNrisFromGlobalRequests().toArray(requests);
+        // Sort the array based off the NRI containing the min requestId in its requests.
+        Arrays.sort(requests,
+                Comparator.comparingInt(nri -> Collections.min(nri.mRequests,
+                        Comparator.comparingInt(req -> req.requestId)).requestId
+                )
+        );
+        return requests;
+    }
+
+    private boolean isLiveNetworkAgent(NetworkAgentInfo nai, int what) {
+        final NetworkAgentInfo officialNai = getNetworkAgentInfoForNetwork(nai.network);
+        if (officialNai != null && officialNai.equals(nai)) return true;
+        if (officialNai != null || VDBG) {
+            loge(eventName(what) + " - isLiveNetworkAgent found mismatched netId: " + officialNai +
+                " - " + nai);
+        }
+        return false;
+    }
+
+    // must be stateless - things change under us.
+    private class NetworkStateTrackerHandler extends Handler {
+        public NetworkStateTrackerHandler(Looper looper) {
+            super(looper);
+        }
+
+        private void maybeHandleNetworkAgentMessage(Message msg) {
+            final Pair<NetworkAgentInfo, Object> arg = (Pair<NetworkAgentInfo, Object>) msg.obj;
+            final NetworkAgentInfo nai = arg.first;
+            if (!mNetworkAgentInfos.contains(nai)) {
+                if (VDBG) {
+                    log(String.format("%s from unknown NetworkAgent", eventName(msg.what)));
+                }
+                return;
+            }
+
+            switch (msg.what) {
+                case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: {
+                    NetworkCapabilities networkCapabilities = (NetworkCapabilities) arg.second;
+                    if (networkCapabilities.hasConnectivityManagedCapability()) {
+                        Log.wtf(TAG, "BUG: " + nai + " has CS-managed capability.");
+                    }
+                    if (networkCapabilities.hasTransport(TRANSPORT_TEST)) {
+                        // Make sure the original object is not mutated. NetworkAgent normally
+                        // makes a copy of the capabilities when sending the message through
+                        // the Messenger, but if this ever changes, not making a defensive copy
+                        // here will give attack vectors to clients using this code path.
+                        networkCapabilities = new NetworkCapabilities(networkCapabilities);
+                        networkCapabilities.restrictCapabilitesForTestNetwork(nai.creatorUid);
+                    }
+                    processCapabilitiesFromAgent(nai, networkCapabilities);
+                    updateCapabilities(nai.getCurrentScore(), nai, networkCapabilities);
+                    break;
+                }
+                case NetworkAgent.EVENT_NETWORK_PROPERTIES_CHANGED: {
+                    LinkProperties newLp = (LinkProperties) arg.second;
+                    processLinkPropertiesFromAgent(nai, newLp);
+                    handleUpdateLinkProperties(nai, newLp);
+                    break;
+                }
+                case NetworkAgent.EVENT_NETWORK_INFO_CHANGED: {
+                    NetworkInfo info = (NetworkInfo) arg.second;
+                    updateNetworkInfo(nai, info);
+                    break;
+                }
+                case NetworkAgent.EVENT_NETWORK_SCORE_CHANGED: {
+                    updateNetworkScore(nai, (NetworkScore) arg.second);
+                    break;
+                }
+                case NetworkAgent.EVENT_SET_EXPLICITLY_SELECTED: {
+                    if (nai.everConnected) {
+                        loge("ERROR: cannot call explicitlySelected on already-connected network");
+                        // Note that if the NAI had been connected, this would affect the
+                        // score, and therefore would require re-mixing the score and performing
+                        // a rematch.
+                    }
+                    nai.networkAgentConfig.explicitlySelected = toBool(msg.arg1);
+                    nai.networkAgentConfig.acceptUnvalidated = toBool(msg.arg1) && toBool(msg.arg2);
+                    // Mark the network as temporarily accepting partial connectivity so that it
+                    // will be validated (and possibly become default) even if it only provides
+                    // partial internet access. Note that if user connects to partial connectivity
+                    // and choose "don't ask again", then wifi disconnected by some reasons(maybe
+                    // out of wifi coverage) and if the same wifi is available again, the device
+                    // will auto connect to this wifi even though the wifi has "no internet".
+                    // TODO: Evaluate using a separate setting in IpMemoryStore.
+                    nai.networkAgentConfig.acceptPartialConnectivity = toBool(msg.arg2);
+                    break;
+                }
+                case NetworkAgent.EVENT_SOCKET_KEEPALIVE: {
+                    mKeepaliveTracker.handleEventSocketKeepalive(nai, msg.arg1, msg.arg2);
+                    break;
+                }
+                case NetworkAgent.EVENT_UNDERLYING_NETWORKS_CHANGED: {
+                    // TODO: prevent loops, e.g., if a network declares itself as underlying.
+                    if (!nai.supportsUnderlyingNetworks()) {
+                        Log.wtf(TAG, "Non-virtual networks cannot have underlying networks");
+                        break;
+                    }
+
+                    final List<Network> underlying = (List<Network>) arg.second;
+
+                    if (isLegacyLockdownNai(nai)
+                            && (underlying == null || underlying.size() != 1)) {
+                        Log.wtf(TAG, "Legacy lockdown VPN " + nai.toShortString()
+                                + " must have exactly one underlying network: " + underlying);
+                    }
+
+                    final Network[] oldUnderlying = nai.declaredUnderlyingNetworks;
+                    nai.declaredUnderlyingNetworks = (underlying != null)
+                            ? underlying.toArray(new Network[0]) : null;
+
+                    if (!Arrays.equals(oldUnderlying, nai.declaredUnderlyingNetworks)) {
+                        if (DBG) {
+                            log(nai.toShortString() + " changed underlying networks to "
+                                    + Arrays.toString(nai.declaredUnderlyingNetworks));
+                        }
+                        updateCapabilitiesForNetwork(nai);
+                        notifyIfacesChangedForNetworkStats();
+                    }
+                    break;
+                }
+                case NetworkAgent.EVENT_TEARDOWN_DELAY_CHANGED: {
+                    if (msg.arg1 >= 0 && msg.arg1 <= NetworkAgent.MAX_TEARDOWN_DELAY_MS) {
+                        nai.teardownDelayMs = msg.arg1;
+                    } else {
+                        logwtf(nai.toShortString() + " set invalid teardown delay " + msg.arg1);
+                    }
+                    break;
+                }
+                case NetworkAgent.EVENT_LINGER_DURATION_CHANGED: {
+                    nai.setLingerDuration((int) arg.second);
+                    break;
+                }
+            }
+        }
+
+        private boolean maybeHandleNetworkMonitorMessage(Message msg) {
+            switch (msg.what) {
+                default:
+                    return false;
+                case EVENT_PROBE_STATUS_CHANGED: {
+                    final Integer netId = (Integer) msg.obj;
+                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
+                    if (nai == null) {
+                        break;
+                    }
+                    final boolean probePrivateDnsCompleted =
+                            ((msg.arg1 & NETWORK_VALIDATION_PROBE_PRIVDNS) != 0);
+                    final boolean privateDnsBroken =
+                            ((msg.arg2 & NETWORK_VALIDATION_PROBE_PRIVDNS) == 0);
+                    if (probePrivateDnsCompleted) {
+                        if (nai.networkCapabilities.isPrivateDnsBroken() != privateDnsBroken) {
+                            nai.networkCapabilities.setPrivateDnsBroken(privateDnsBroken);
+                            updateCapabilitiesForNetwork(nai);
+                        }
+                        // Only show the notification when the private DNS is broken and the
+                        // PRIVATE_DNS_BROKEN notification hasn't shown since last valid.
+                        if (privateDnsBroken && !nai.networkAgentConfig.hasShownBroken) {
+                            showNetworkNotification(nai, NotificationType.PRIVATE_DNS_BROKEN);
+                        }
+                        nai.networkAgentConfig.hasShownBroken = privateDnsBroken;
+                    } else if (nai.networkCapabilities.isPrivateDnsBroken()) {
+                        // If probePrivateDnsCompleted is false but nai.networkCapabilities says
+                        // private DNS is broken, it means this network is being reevaluated.
+                        // Either probing private DNS is not necessary any more or it hasn't been
+                        // done yet. In either case, the networkCapabilities should be updated to
+                        // reflect the new status.
+                        nai.networkCapabilities.setPrivateDnsBroken(false);
+                        updateCapabilitiesForNetwork(nai);
+                        nai.networkAgentConfig.hasShownBroken = false;
+                    }
+                    break;
+                }
+                case EVENT_NETWORK_TESTED: {
+                    final NetworkTestedResults results = (NetworkTestedResults) msg.obj;
+
+                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(results.mNetId);
+                    if (nai == null) break;
+
+                    handleNetworkTested(nai, results.mTestResult,
+                            (results.mRedirectUrl == null) ? "" : results.mRedirectUrl);
+                    break;
+                }
+                case EVENT_PROVISIONING_NOTIFICATION: {
+                    final int netId = msg.arg2;
+                    final boolean visible = toBool(msg.arg1);
+                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
+                    // If captive portal status has changed, update capabilities or disconnect.
+                    if (nai != null && (visible != nai.lastCaptivePortalDetected)) {
+                        nai.lastCaptivePortalDetected = visible;
+                        nai.everCaptivePortalDetected |= visible;
+                        if (nai.lastCaptivePortalDetected &&
+                                ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID
+                                        == getCaptivePortalMode()) {
+                            if (DBG) log("Avoiding captive portal network: " + nai.toShortString());
+                            nai.onPreventAutomaticReconnect();
+                            teardownUnneededNetwork(nai);
+                            break;
+                        }
+                        updateCapabilitiesForNetwork(nai);
+                    }
+                    if (!visible) {
+                        // Only clear SIGN_IN and NETWORK_SWITCH notifications here, or else other
+                        // notifications belong to the same network may be cleared unexpectedly.
+                        mNotifier.clearNotification(netId, NotificationType.SIGN_IN);
+                        mNotifier.clearNotification(netId, NotificationType.NETWORK_SWITCH);
+                    } else {
+                        if (nai == null) {
+                            loge("EVENT_PROVISIONING_NOTIFICATION from unknown NetworkMonitor");
+                            break;
+                        }
+                        if (!nai.networkAgentConfig.provisioningNotificationDisabled) {
+                            mNotifier.showNotification(netId, NotificationType.SIGN_IN, nai, null,
+                                    (PendingIntent) msg.obj,
+                                    nai.networkAgentConfig.explicitlySelected);
+                        }
+                    }
+                    break;
+                }
+                case EVENT_PRIVATE_DNS_CONFIG_RESOLVED: {
+                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
+                    if (nai == null) break;
+
+                    updatePrivateDns(nai, (PrivateDnsConfig) msg.obj);
+                    break;
+                }
+                case EVENT_CAPPORT_DATA_CHANGED: {
+                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
+                    if (nai == null) break;
+                    handleCapportApiDataUpdate(nai, (CaptivePortalData) msg.obj);
+                    break;
+                }
+            }
+            return true;
+        }
+
+        private void handleNetworkTested(
+                @NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
+            final boolean wasPartial = nai.partialConnectivity;
+            nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
+            final boolean partialConnectivityChanged =
+                    (wasPartial != nai.partialConnectivity);
+
+            final boolean valid = ((testResult & NETWORK_VALIDATION_RESULT_VALID) != 0);
+            final boolean wasValidated = nai.lastValidated;
+            final boolean wasDefault = isDefaultNetwork(nai);
+
+            if (DBG) {
+                final String logMsg = !TextUtils.isEmpty(redirectUrl)
+                        ? " with redirect to " + redirectUrl
+                        : "";
+                log(nai.toShortString() + " validation " + (valid ? "passed" : "failed") + logMsg);
+            }
+            if (valid != nai.lastValidated) {
+                final int oldScore = nai.getCurrentScore();
+                nai.lastValidated = valid;
+                nai.everValidated |= valid;
+                updateCapabilities(oldScore, nai, nai.networkCapabilities);
+                if (valid) {
+                    handleFreshlyValidatedNetwork(nai);
+                    // Clear NO_INTERNET, PRIVATE_DNS_BROKEN, PARTIAL_CONNECTIVITY and
+                    // LOST_INTERNET notifications if network becomes valid.
+                    mNotifier.clearNotification(nai.network.getNetId(),
+                            NotificationType.NO_INTERNET);
+                    mNotifier.clearNotification(nai.network.getNetId(),
+                            NotificationType.LOST_INTERNET);
+                    mNotifier.clearNotification(nai.network.getNetId(),
+                            NotificationType.PARTIAL_CONNECTIVITY);
+                    mNotifier.clearNotification(nai.network.getNetId(),
+                            NotificationType.PRIVATE_DNS_BROKEN);
+                    // If network becomes valid, the hasShownBroken should be reset for
+                    // that network so that the notification will be fired when the private
+                    // DNS is broken again.
+                    nai.networkAgentConfig.hasShownBroken = false;
+                }
+            } else if (partialConnectivityChanged) {
+                updateCapabilitiesForNetwork(nai);
+            }
+            updateInetCondition(nai);
+            // Let the NetworkAgent know the state of its network
+            // TODO: Evaluate to update partial connectivity to status to NetworkAgent.
+            nai.onValidationStatusChanged(
+                    valid ? NetworkAgent.VALID_NETWORK : NetworkAgent.INVALID_NETWORK,
+                    redirectUrl);
+
+            // If NetworkMonitor detects partial connectivity before
+            // EVENT_PROMPT_UNVALIDATED arrives, show the partial connectivity notification
+            // immediately. Re-notify partial connectivity silently if no internet
+            // notification already there.
+            if (!wasPartial && nai.partialConnectivity) {
+                // Remove delayed message if there is a pending message.
+                mHandler.removeMessages(EVENT_PROMPT_UNVALIDATED, nai.network);
+                handlePromptUnvalidated(nai.network);
+            }
+
+            if (wasValidated && !nai.lastValidated) {
+                handleNetworkUnvalidated(nai);
+            }
+        }
+
+        private int getCaptivePortalMode() {
+            return Settings.Global.getInt(mContext.getContentResolver(),
+                    ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE,
+                    ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT);
+        }
+
+        private boolean maybeHandleNetworkAgentInfoMessage(Message msg) {
+            switch (msg.what) {
+                default:
+                    return false;
+                case NetworkAgentInfo.EVENT_NETWORK_LINGER_COMPLETE: {
+                    NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj;
+                    if (nai != null && isLiveNetworkAgent(nai, msg.what)) {
+                        handleLingerComplete(nai);
+                    }
+                    break;
+                }
+                case NetworkAgentInfo.EVENT_AGENT_REGISTERED: {
+                    handleNetworkAgentRegistered(msg);
+                    break;
+                }
+                case NetworkAgentInfo.EVENT_AGENT_DISCONNECTED: {
+                    handleNetworkAgentDisconnected(msg);
+                    break;
+                }
+            }
+            return true;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            if (!maybeHandleNetworkMonitorMessage(msg)
+                    && !maybeHandleNetworkAgentInfoMessage(msg)) {
+                maybeHandleNetworkAgentMessage(msg);
+            }
+        }
+    }
+
+    private class NetworkMonitorCallbacks extends INetworkMonitorCallbacks.Stub {
+        private final int mNetId;
+        private final AutodestructReference<NetworkAgentInfo> mNai;
+
+        private NetworkMonitorCallbacks(NetworkAgentInfo nai) {
+            mNetId = nai.network.getNetId();
+            mNai = new AutodestructReference<>(nai);
+        }
+
+        @Override
+        public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) {
+            mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT,
+                    new Pair<>(mNai.getAndDestroy(), networkMonitor)));
+        }
+
+        @Override
+        public void notifyNetworkTested(int testResult, @Nullable String redirectUrl) {
+            // Legacy version of notifyNetworkTestedWithExtras.
+            // Would only be called if the system has a NetworkStack module older than the
+            // framework, which does not happen in practice.
+            Log.wtf(TAG, "Deprecated notifyNetworkTested called: no action taken");
+        }
+
+        @Override
+        public void notifyNetworkTestedWithExtras(NetworkTestResultParcelable p) {
+            // Notify mTrackerHandler and mConnectivityDiagnosticsHandler of the event. Both use
+            // the same looper so messages will be processed in sequence.
+            final Message msg = mTrackerHandler.obtainMessage(
+                    EVENT_NETWORK_TESTED,
+                    new NetworkTestedResults(
+                            mNetId, p.result, p.timestampMillis, p.redirectUrl));
+            mTrackerHandler.sendMessage(msg);
+
+            // Invoke ConnectivityReport generation for this Network test event.
+            final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(mNetId);
+            if (nai == null) return;
+
+            final PersistableBundle extras = new PersistableBundle();
+            extras.putInt(KEY_NETWORK_VALIDATION_RESULT, p.result);
+            extras.putInt(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK, p.probesSucceeded);
+            extras.putInt(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK, p.probesAttempted);
+
+            ConnectivityReportEvent reportEvent =
+                    new ConnectivityReportEvent(p.timestampMillis, nai, extras);
+            final Message m = mConnectivityDiagnosticsHandler.obtainMessage(
+                    ConnectivityDiagnosticsHandler.EVENT_NETWORK_TESTED, reportEvent);
+            mConnectivityDiagnosticsHandler.sendMessage(m);
+        }
+
+        @Override
+        public void notifyPrivateDnsConfigResolved(PrivateDnsConfigParcel config) {
+            mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+                    EVENT_PRIVATE_DNS_CONFIG_RESOLVED,
+                    0, mNetId, PrivateDnsConfig.fromParcel(config)));
+        }
+
+        @Override
+        public void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) {
+            mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+                    EVENT_PROBE_STATUS_CHANGED,
+                    probesCompleted, probesSucceeded, new Integer(mNetId)));
+        }
+
+        @Override
+        public void notifyCaptivePortalDataChanged(CaptivePortalData data) {
+            mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+                    EVENT_CAPPORT_DATA_CHANGED,
+                    0, mNetId, data));
+        }
+
+        @Override
+        public void showProvisioningNotification(String action, String packageName) {
+            final Intent intent = new Intent(action);
+            intent.setPackage(packageName);
+
+            final PendingIntent pendingIntent;
+            // Only the system server can register notifications with package "android"
+            final long token = Binder.clearCallingIdentity();
+            try {
+                pendingIntent = PendingIntent.getBroadcast(
+                        mContext,
+                        0 /* requestCode */,
+                        intent,
+                        PendingIntent.FLAG_IMMUTABLE);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+            mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+                    EVENT_PROVISIONING_NOTIFICATION, PROVISIONING_NOTIFICATION_SHOW,
+                    mNetId, pendingIntent));
+        }
+
+        @Override
+        public void hideProvisioningNotification() {
+            mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+                    EVENT_PROVISIONING_NOTIFICATION, PROVISIONING_NOTIFICATION_HIDE, mNetId));
+        }
+
+        @Override
+        public void notifyDataStallSuspected(DataStallReportParcelable p) {
+            ConnectivityService.this.notifyDataStallSuspected(p, mNetId);
+        }
+
+        @Override
+        public int getInterfaceVersion() {
+            return this.VERSION;
+        }
+
+        @Override
+        public String getInterfaceHash() {
+            return this.HASH;
+        }
+    }
+
+    private void notifyDataStallSuspected(DataStallReportParcelable p, int netId) {
+        log("Data stall detected with methods: " + p.detectionMethod);
+
+        final PersistableBundle extras = new PersistableBundle();
+        int detectionMethod = 0;
+        if (hasDataStallDetectionMethod(p, DETECTION_METHOD_DNS_EVENTS)) {
+            extras.putInt(KEY_DNS_CONSECUTIVE_TIMEOUTS, p.dnsConsecutiveTimeouts);
+            detectionMethod |= DETECTION_METHOD_DNS_EVENTS;
+        }
+        if (hasDataStallDetectionMethod(p, DETECTION_METHOD_TCP_METRICS)) {
+            extras.putInt(KEY_TCP_PACKET_FAIL_RATE, p.tcpPacketFailRate);
+            extras.putInt(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS,
+                    p.tcpMetricsCollectionPeriodMillis);
+            detectionMethod |= DETECTION_METHOD_TCP_METRICS;
+        }
+
+        final Message msg = mConnectivityDiagnosticsHandler.obtainMessage(
+                ConnectivityDiagnosticsHandler.EVENT_DATA_STALL_SUSPECTED, detectionMethod, netId,
+                new Pair<>(p.timestampMillis, extras));
+
+        // NetworkStateTrackerHandler currently doesn't take any actions based on data
+        // stalls so send the message directly to ConnectivityDiagnosticsHandler and avoid
+        // the cost of going through two handlers.
+        mConnectivityDiagnosticsHandler.sendMessage(msg);
+    }
+
+    private boolean hasDataStallDetectionMethod(DataStallReportParcelable p, int detectionMethod) {
+        return (p.detectionMethod & detectionMethod) != 0;
+    }
+
+    private boolean networkRequiresPrivateDnsValidation(NetworkAgentInfo nai) {
+        return isPrivateDnsValidationRequired(nai.networkCapabilities);
+    }
+
+    private void handleFreshlyValidatedNetwork(NetworkAgentInfo nai) {
+        if (nai == null) return;
+        // If the Private DNS mode is opportunistic, reprogram the DNS servers
+        // in order to restart a validation pass from within netd.
+        final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig();
+        if (cfg.useTls && TextUtils.isEmpty(cfg.hostname)) {
+            updateDnses(nai.linkProperties, null, nai.network.getNetId());
+        }
+    }
+
+    private void handlePrivateDnsSettingsChanged() {
+        final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig();
+
+        for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+            handlePerNetworkPrivateDnsConfig(nai, cfg);
+            if (networkRequiresPrivateDnsValidation(nai)) {
+                handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
+            }
+        }
+    }
+
+    private void handlePerNetworkPrivateDnsConfig(NetworkAgentInfo nai, PrivateDnsConfig cfg) {
+        // Private DNS only ever applies to networks that might provide
+        // Internet access and therefore also require validation.
+        if (!networkRequiresPrivateDnsValidation(nai)) return;
+
+        // Notify the NetworkAgentInfo/NetworkMonitor in case NetworkMonitor needs to cancel or
+        // schedule DNS resolutions. If a DNS resolution is required the
+        // result will be sent back to us.
+        nai.networkMonitor().notifyPrivateDnsChanged(cfg.toParcel());
+
+        // With Private DNS bypass support, we can proceed to update the
+        // Private DNS config immediately, even if we're in strict mode
+        // and have not yet resolved the provider name into a set of IPs.
+        updatePrivateDns(nai, cfg);
+    }
+
+    private void updatePrivateDns(NetworkAgentInfo nai, PrivateDnsConfig newCfg) {
+        mDnsManager.updatePrivateDns(nai.network, newCfg);
+        updateDnses(nai.linkProperties, null, nai.network.getNetId());
+    }
+
+    private void handlePrivateDnsValidationUpdate(PrivateDnsValidationUpdate update) {
+        NetworkAgentInfo nai = getNetworkAgentInfoForNetId(update.netId);
+        if (nai == null) {
+            return;
+        }
+        mDnsManager.updatePrivateDnsValidation(update);
+        handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
+    }
+
+    private void handleNat64PrefixEvent(int netId, int operation, String prefixAddress,
+            int prefixLength) {
+        NetworkAgentInfo nai = mNetworkForNetId.get(netId);
+        if (nai == null) return;
+
+        log(String.format("NAT64 prefix changed on netId %d: operation=%d, %s/%d",
+                netId, operation, prefixAddress, prefixLength));
+
+        IpPrefix prefix = null;
+        if (operation == IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_ADDED) {
+            try {
+                prefix = new IpPrefix(InetAddresses.parseNumericAddress(prefixAddress),
+                        prefixLength);
+            } catch (IllegalArgumentException e) {
+                loge("Invalid NAT64 prefix " + prefixAddress + "/" + prefixLength);
+                return;
+            }
+        }
+
+        nai.clatd.setNat64PrefixFromDns(prefix);
+        handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
+    }
+
+    private void handleCapportApiDataUpdate(@NonNull final NetworkAgentInfo nai,
+            @Nullable final CaptivePortalData data) {
+        nai.capportApiData = data;
+        // CaptivePortalData will be merged into LinkProperties from NetworkAgentInfo
+        handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
+    }
+
+    /**
+     * Updates the inactivity state from the network requests inside the NAI.
+     * @param nai the agent info to update
+     * @param now the timestamp of the event causing this update
+     * @return whether the network was inactive as a result of this update
+     */
+    private boolean updateInactivityState(@NonNull final NetworkAgentInfo nai, final long now) {
+        // 1. Update the inactivity timer. If it's changed, reschedule or cancel the alarm.
+        // 2. If the network was inactive and there are now requests, unset inactive.
+        // 3. If this network is unneeded (which implies it is not lingering), and there is at least
+        //    one lingered request, set inactive.
+        nai.updateInactivityTimer();
+        if (nai.isInactive() && nai.numForegroundNetworkRequests() > 0) {
+            if (DBG) log("Unsetting inactive " + nai.toShortString());
+            nai.unsetInactive();
+            logNetworkEvent(nai, NetworkEvent.NETWORK_UNLINGER);
+        } else if (unneeded(nai, UnneededFor.LINGER) && nai.getInactivityExpiry() > 0) {
+            if (DBG) {
+                final int lingerTime = (int) (nai.getInactivityExpiry() - now);
+                log("Setting inactive " + nai.toShortString() + " for " + lingerTime + "ms");
+            }
+            nai.setInactive();
+            logNetworkEvent(nai, NetworkEvent.NETWORK_LINGER);
+            return true;
+        }
+        return false;
+    }
+
+    private void handleNetworkAgentRegistered(Message msg) {
+        final NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj;
+        if (!mNetworkAgentInfos.contains(nai)) {
+            return;
+        }
+
+        if (msg.arg1 == NetworkAgentInfo.ARG_AGENT_SUCCESS) {
+            if (VDBG) log("NetworkAgent registered");
+        } else {
+            loge("Error connecting NetworkAgent");
+            mNetworkAgentInfos.remove(nai);
+            if (nai != null) {
+                final boolean wasDefault = isDefaultNetwork(nai);
+                synchronized (mNetworkForNetId) {
+                    mNetworkForNetId.remove(nai.network.getNetId());
+                }
+                mNetIdManager.releaseNetId(nai.network.getNetId());
+                // Just in case.
+                mLegacyTypeTracker.remove(nai, wasDefault);
+            }
+        }
+    }
+
+    private void handleNetworkAgentDisconnected(Message msg) {
+        NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj;
+        if (mNetworkAgentInfos.contains(nai)) {
+            disconnectAndDestroyNetwork(nai);
+        }
+    }
+
+    // Destroys a network, remove references to it from the internal state managed by
+    // ConnectivityService, free its interfaces and clean up.
+    // Must be called on the Handler thread.
+    private void disconnectAndDestroyNetwork(NetworkAgentInfo nai) {
+        ensureRunningOnConnectivityServiceThread();
+        if (DBG) {
+            log(nai.toShortString() + " disconnected, was satisfying " + nai.numNetworkRequests());
+        }
+        // Clear all notifications of this network.
+        mNotifier.clearNotification(nai.network.getNetId());
+        // A network agent has disconnected.
+        // TODO - if we move the logic to the network agent (have them disconnect
+        // because they lost all their requests or because their score isn't good)
+        // then they would disconnect organically, report their new state and then
+        // disconnect the channel.
+        if (nai.networkInfo.isConnected()) {
+            nai.networkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED,
+                    null, null);
+        }
+        final boolean wasDefault = isDefaultNetwork(nai);
+        if (wasDefault) {
+            mDefaultInetConditionPublished = 0;
+        }
+        notifyIfacesChangedForNetworkStats();
+        // TODO - we shouldn't send CALLBACK_LOST to requests that can be satisfied
+        // by other networks that are already connected. Perhaps that can be done by
+        // sending all CALLBACK_LOST messages (for requests, not listens) at the end
+        // of rematchAllNetworksAndRequests
+        notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOST);
+        mKeepaliveTracker.handleStopAllKeepalives(nai, SocketKeepalive.ERROR_INVALID_NETWORK);
+
+        mQosCallbackTracker.handleNetworkReleased(nai.network);
+        for (String iface : nai.linkProperties.getAllInterfaceNames()) {
+            // Disable wakeup packet monitoring for each interface.
+            wakeupModifyInterface(iface, nai.networkCapabilities, false);
+        }
+        nai.networkMonitor().notifyNetworkDisconnected();
+        mNetworkAgentInfos.remove(nai);
+        nai.clatd.update();
+        synchronized (mNetworkForNetId) {
+            // Remove the NetworkAgent, but don't mark the netId as
+            // available until we've told netd to delete it below.
+            mNetworkForNetId.remove(nai.network.getNetId());
+        }
+        propagateUnderlyingNetworkCapabilities(nai.network);
+        // Remove all previously satisfied requests.
+        for (int i = 0; i < nai.numNetworkRequests(); i++) {
+            final NetworkRequest request = nai.requestAt(i);
+            final NetworkRequestInfo nri = mNetworkRequests.get(request);
+            final NetworkAgentInfo currentNetwork = nri.getSatisfier();
+            if (currentNetwork != null
+                    && currentNetwork.network.getNetId() == nai.network.getNetId()) {
+                // uid rules for this network will be removed in destroyNativeNetwork(nai).
+                // TODO : setting the satisfier is in fact the job of the rematch. Teach the
+                // rematch not to keep disconnected agents instead of setting it here ; this
+                // will also allow removing updating the offers below.
+                nri.setSatisfier(null, null);
+                for (final NetworkOfferInfo noi : mNetworkOffers) {
+                    informOffer(nri, noi.offer, mNetworkRanker);
+                }
+
+                if (mDefaultRequest == nri) {
+                    // TODO : make battery stats aware that since 2013 multiple interfaces may be
+                    //  active at the same time. For now keep calling this with the default
+                    //  network, because while incorrect this is the closest to the old (also
+                    //  incorrect) behavior.
+                    mNetworkActivityTracker.updateDataActivityTracking(
+                            null /* newNetwork */, nai);
+                    ensureNetworkTransitionWakelock(nai.toShortString());
+                }
+            }
+        }
+        nai.clearInactivityState();
+        // TODO: mLegacyTypeTracker.remove seems redundant given there's a full rematch right after.
+        //  Currently, deleting it breaks tests that check for the default network disconnecting.
+        //  Find out why, fix the rematch code, and delete this.
+        mLegacyTypeTracker.remove(nai, wasDefault);
+        rematchAllNetworksAndRequests();
+        mLingerMonitor.noteDisconnect(nai);
+
+        // Immediate teardown.
+        if (nai.teardownDelayMs == 0) {
+            destroyNetwork(nai);
+            return;
+        }
+
+        // Delayed teardown.
+        try {
+            mNetd.networkSetPermissionForNetwork(nai.network.netId, INetd.PERMISSION_SYSTEM);
+        } catch (RemoteException e) {
+            Log.d(TAG, "Error marking network restricted during teardown: " + e);
+        }
+        mHandler.postDelayed(() -> destroyNetwork(nai), nai.teardownDelayMs);
+    }
+
+    private void destroyNetwork(NetworkAgentInfo nai) {
+        if (nai.created) {
+            // Tell netd to clean up the configuration for this network
+            // (routing rules, DNS, etc).
+            // This may be slow as it requires a lot of netd shelling out to ip and
+            // ip[6]tables to flush routes and remove the incoming packet mark rule, so do it
+            // after we've rematched networks with requests (which might change the default
+            // network or service a new request from an app), so network traffic isn't interrupted
+            // for an unnecessarily long time.
+            destroyNativeNetwork(nai);
+            mDnsManager.removeNetwork(nai.network);
+        }
+        mNetIdManager.releaseNetId(nai.network.getNetId());
+        nai.onNetworkDestroyed();
+    }
+
+    private boolean createNativeNetwork(@NonNull NetworkAgentInfo nai) {
+        try {
+            // This should never fail.  Specifying an already in use NetID will cause failure.
+            final NativeNetworkConfig config;
+            if (nai.isVPN()) {
+                if (getVpnType(nai) == VpnManager.TYPE_VPN_NONE) {
+                    Log.wtf(TAG, "Unable to get VPN type from network " + nai.toShortString());
+                    return false;
+                }
+                config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.VIRTUAL,
+                        INetd.PERMISSION_NONE,
+                        (nai.networkAgentConfig == null || !nai.networkAgentConfig.allowBypass),
+                        getVpnType(nai));
+            } else {
+                config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL,
+                        getNetworkPermission(nai.networkCapabilities), /*secure=*/ false,
+                        VpnManager.TYPE_VPN_NONE);
+            }
+            mNetd.networkCreate(config);
+            mDnsResolver.createNetworkCache(nai.network.getNetId());
+            mDnsManager.updateTransportsForNetwork(nai.network.getNetId(),
+                    nai.networkCapabilities.getTransportTypes());
+            return true;
+        } catch (RemoteException | ServiceSpecificException e) {
+            loge("Error creating network " + nai.toShortString() + ": " + e.getMessage());
+            return false;
+        }
+    }
+
+    private void destroyNativeNetwork(@NonNull NetworkAgentInfo nai) {
+        try {
+            mNetd.networkDestroy(nai.network.getNetId());
+        } catch (RemoteException | ServiceSpecificException e) {
+            loge("Exception destroying network(networkDestroy): " + e);
+        }
+        try {
+            mDnsResolver.destroyNetworkCache(nai.network.getNetId());
+        } catch (RemoteException | ServiceSpecificException e) {
+            loge("Exception destroying network: " + e);
+        }
+    }
+
+    // If this method proves to be too slow then we can maintain a separate
+    // pendingIntent => NetworkRequestInfo map.
+    // This method assumes that every non-null PendingIntent maps to exactly 1 NetworkRequestInfo.
+    private NetworkRequestInfo findExistingNetworkRequestInfo(PendingIntent pendingIntent) {
+        for (Map.Entry<NetworkRequest, NetworkRequestInfo> entry : mNetworkRequests.entrySet()) {
+            PendingIntent existingPendingIntent = entry.getValue().mPendingIntent;
+            if (existingPendingIntent != null &&
+                    mDeps.intentFilterEquals(existingPendingIntent, pendingIntent)) {
+                return entry.getValue();
+            }
+        }
+        return null;
+    }
+
+    private void handleRegisterNetworkRequestWithIntent(@NonNull final Message msg) {
+        final NetworkRequestInfo nri = (NetworkRequestInfo) (msg.obj);
+        // handleRegisterNetworkRequestWithIntent() doesn't apply to multilayer requests.
+        ensureNotMultilayerRequest(nri, "handleRegisterNetworkRequestWithIntent");
+        final NetworkRequestInfo existingRequest =
+                findExistingNetworkRequestInfo(nri.mPendingIntent);
+        if (existingRequest != null) { // remove the existing request.
+            if (DBG) {
+                log("Replacing " + existingRequest.mRequests.get(0) + " with "
+                        + nri.mRequests.get(0) + " because their intents matched.");
+            }
+            handleReleaseNetworkRequest(existingRequest.mRequests.get(0), mDeps.getCallingUid(),
+                    /* callOnUnavailable */ false);
+        }
+        handleRegisterNetworkRequest(nri);
+    }
+
+    private void handleRegisterNetworkRequest(@NonNull final NetworkRequestInfo nri) {
+        handleRegisterNetworkRequests(Collections.singleton(nri));
+    }
+
+    private void handleRegisterNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) {
+        ensureRunningOnConnectivityServiceThread();
+        for (final NetworkRequestInfo nri : nris) {
+            mNetworkRequestInfoLogs.log("REGISTER " + nri);
+            for (final NetworkRequest req : nri.mRequests) {
+                mNetworkRequests.put(req, nri);
+                // TODO: Consider update signal strength for other types.
+                if (req.isListen()) {
+                    for (final NetworkAgentInfo network : mNetworkAgentInfos) {
+                        if (req.networkCapabilities.hasSignalStrength()
+                                && network.satisfiesImmutableCapabilitiesOf(req)) {
+                            updateSignalStrengthThresholds(network, "REGISTER", req);
+                        }
+                    }
+                }
+            }
+            // If this NRI has a satisfier already, it is replacing an older request that
+            // has been removed. Track it.
+            final NetworkRequest activeRequest = nri.getActiveRequest();
+            if (null != activeRequest) {
+                // If there is an active request, then for sure there is a satisfier.
+                nri.getSatisfier().addRequest(activeRequest);
+            }
+        }
+
+        rematchAllNetworksAndRequests();
+
+        // Requests that have not been matched to a network will not have been sent to the
+        // providers, because the old satisfier and the new satisfier are the same (null in this
+        // case). Send these requests to the providers.
+        for (final NetworkRequestInfo nri : nris) {
+            for (final NetworkOfferInfo noi : mNetworkOffers) {
+                informOffer(nri, noi.offer, mNetworkRanker);
+            }
+        }
+    }
+
+    private void handleReleaseNetworkRequestWithIntent(@NonNull final PendingIntent pendingIntent,
+            final int callingUid) {
+        final NetworkRequestInfo nri = findExistingNetworkRequestInfo(pendingIntent);
+        if (nri != null) {
+            // handleReleaseNetworkRequestWithIntent() paths don't apply to multilayer requests.
+            ensureNotMultilayerRequest(nri, "handleReleaseNetworkRequestWithIntent");
+            handleReleaseNetworkRequest(
+                    nri.mRequests.get(0),
+                    callingUid,
+                    /* callOnUnavailable */ false);
+        }
+    }
+
+    // Determines whether the network is the best (or could become the best, if it validated), for
+    // none of a particular type of NetworkRequests. The type of NetworkRequests considered depends
+    // on the value of reason:
+    //
+    // - UnneededFor.TEARDOWN: non-listen NetworkRequests. If a network is unneeded for this reason,
+    //   then it should be torn down.
+    // - UnneededFor.LINGER: foreground NetworkRequests. If a network is unneeded for this reason,
+    //   then it should be lingered.
+    private boolean unneeded(NetworkAgentInfo nai, UnneededFor reason) {
+        ensureRunningOnConnectivityServiceThread();
+
+        if (!nai.everConnected || nai.isVPN() || nai.isInactive()
+                || nai.getScore().getKeepConnectedReason() != NetworkScore.KEEP_CONNECTED_NONE) {
+            return false;
+        }
+
+        final int numRequests;
+        switch (reason) {
+            case TEARDOWN:
+                numRequests = nai.numRequestNetworkRequests();
+                break;
+            case LINGER:
+                numRequests = nai.numForegroundNetworkRequests();
+                break;
+            default:
+                Log.wtf(TAG, "Invalid reason. Cannot happen.");
+                return true;
+        }
+
+        if (numRequests > 0) return false;
+
+        for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+            if (reason == UnneededFor.LINGER
+                    && !nri.isMultilayerRequest()
+                    && nri.mRequests.get(0).isBackgroundRequest()) {
+                // Background requests don't affect lingering.
+                continue;
+            }
+
+            if (isNetworkPotentialSatisfier(nai, nri)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean isNetworkPotentialSatisfier(
+            @NonNull final NetworkAgentInfo candidate, @NonNull final NetworkRequestInfo nri) {
+        // listen requests won't keep up a network satisfying it. If this is not a multilayer
+        // request, return immediately. For multilayer requests, check to see if any of the
+        // multilayer requests may have a potential satisfier.
+        if (!nri.isMultilayerRequest() && (nri.mRequests.get(0).isListen()
+                || nri.mRequests.get(0).isListenForBest())) {
+            return false;
+        }
+        for (final NetworkRequest req : nri.mRequests) {
+            // This multilayer listen request is satisfied therefore no further requests need to be
+            // evaluated deeming this network not a potential satisfier.
+            if ((req.isListen() || req.isListenForBest()) && nri.getActiveRequest() == req) {
+                return false;
+            }
+            // As non-multilayer listen requests have already returned, the below would only happen
+            // for a multilayer request therefore continue to the next request if available.
+            if (req.isListen() || req.isListenForBest()) {
+                continue;
+            }
+            // If this Network is already the highest scoring Network for a request, or if
+            // there is hope for it to become one if it validated, then it is needed.
+            if (candidate.satisfies(req)) {
+                // As soon as a network is found that satisfies a request, return. Specifically for
+                // multilayer requests, returning as soon as a NetworkAgentInfo satisfies a request
+                // is important so as to not evaluate lower priority requests further in
+                // nri.mRequests.
+                final NetworkAgentInfo champion = req.equals(nri.getActiveRequest())
+                        ? nri.getSatisfier() : null;
+                // Note that this catches two important cases:
+                // 1. Unvalidated cellular will not be reaped when unvalidated WiFi
+                //    is currently satisfying the request.  This is desirable when
+                //    cellular ends up validating but WiFi does not.
+                // 2. Unvalidated WiFi will not be reaped when validated cellular
+                //    is currently satisfying the request.  This is desirable when
+                //    WiFi ends up validating and out scoring cellular.
+                return mNetworkRanker.mightBeat(req, champion, candidate.getValidatedScoreable());
+            }
+        }
+
+        return false;
+    }
+
+    private NetworkRequestInfo getNriForAppRequest(
+            NetworkRequest request, int callingUid, String requestedOperation) {
+        // Looking up the app passed param request in mRequests isn't possible since it may return
+        // null for a request managed by a per-app default. Therefore use getNriForAppRequest() to
+        // do the lookup since that will also find per-app default managed requests.
+        // Additionally, this lookup needs to be relatively fast (hence the lookup optimization)
+        // to avoid potential race conditions when validating a package->uid mapping when sending
+        // the callback on the very low-chance that an application shuts down prior to the callback
+        // being sent.
+        final NetworkRequestInfo nri = mNetworkRequests.get(request) != null
+                ? mNetworkRequests.get(request) : getNriForAppRequest(request);
+
+        if (nri != null) {
+            if (Process.SYSTEM_UID != callingUid && nri.mUid != callingUid) {
+                log(String.format("UID %d attempted to %s for unowned request %s",
+                        callingUid, requestedOperation, nri));
+                return null;
+            }
+        }
+
+        return nri;
+    }
+
+    private void ensureNotMultilayerRequest(@NonNull final NetworkRequestInfo nri,
+            final String callingMethod) {
+        if (nri.isMultilayerRequest()) {
+            throw new IllegalStateException(
+                    callingMethod + " does not support multilayer requests.");
+        }
+    }
+
+    private void handleTimedOutNetworkRequest(@NonNull final NetworkRequestInfo nri) {
+        ensureRunningOnConnectivityServiceThread();
+        // handleTimedOutNetworkRequest() is part of the requestNetwork() flow which works off of a
+        // single NetworkRequest and thus does not apply to multilayer requests.
+        ensureNotMultilayerRequest(nri, "handleTimedOutNetworkRequest");
+        if (mNetworkRequests.get(nri.mRequests.get(0)) == null) {
+            return;
+        }
+        if (nri.isBeingSatisfied()) {
+            return;
+        }
+        if (VDBG || (DBG && nri.mRequests.get(0).isRequest())) {
+            log("releasing " + nri.mRequests.get(0) + " (timeout)");
+        }
+        handleRemoveNetworkRequest(nri);
+        callCallbackForRequest(
+                nri, null, ConnectivityManager.CALLBACK_UNAVAIL, 0);
+    }
+
+    private void handleReleaseNetworkRequest(@NonNull final NetworkRequest request,
+            final int callingUid,
+            final boolean callOnUnavailable) {
+        final NetworkRequestInfo nri =
+                getNriForAppRequest(request, callingUid, "release NetworkRequest");
+        if (nri == null) {
+            return;
+        }
+        if (VDBG || (DBG && request.isRequest())) {
+            log("releasing " + request + " (release request)");
+        }
+        handleRemoveNetworkRequest(nri);
+        if (callOnUnavailable) {
+            callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_UNAVAIL, 0);
+        }
+    }
+
+    private void handleRemoveNetworkRequest(@NonNull final NetworkRequestInfo nri) {
+        ensureRunningOnConnectivityServiceThread();
+        nri.unlinkDeathRecipient();
+        for (final NetworkRequest req : nri.mRequests) {
+            mNetworkRequests.remove(req);
+            if (req.isListen()) {
+                removeListenRequestFromNetworks(req);
+            }
+        }
+        if (mDefaultNetworkRequests.remove(nri)) {
+            // If this request was one of the defaults, then the UID rules need to be updated
+            // WARNING : if the app(s) for which this network request is the default are doing
+            // traffic, this will kill their connected sockets, even if an equivalent request
+            // is going to be reinstated right away ; unconnected traffic will go on the default
+            // until the new default is set, which will happen very soon.
+            // TODO : The only way out of this is to diff old defaults and new defaults, and only
+            // remove ranges for those requests that won't have a replacement
+            final NetworkAgentInfo satisfier = nri.getSatisfier();
+            if (null != satisfier) {
+                try {
+                    // TODO: Passing default network priority to netd.
+                    mNetd.networkRemoveUidRanges(satisfier.network.getNetId(),
+                            toUidRangeStableParcels(nri.getUids())
+                            /* nri.getDefaultNetworkPriority() */);
+                } catch (RemoteException e) {
+                    loge("Exception setting network preference default network", e);
+                }
+            }
+        }
+        nri.decrementRequestCount();
+        mNetworkRequestInfoLogs.log("RELEASE " + nri);
+
+        if (null != nri.getActiveRequest()) {
+            if (!nri.getActiveRequest().isListen()) {
+                removeSatisfiedNetworkRequestFromNetwork(nri);
+            } else {
+                nri.setSatisfier(null, null);
+            }
+        }
+
+        // For all outstanding offers, cancel any of the layers of this NRI that used to be
+        // needed for this offer.
+        for (final NetworkOfferInfo noi : mNetworkOffers) {
+            for (final NetworkRequest req : nri.mRequests) {
+                if (req.isRequest() && noi.offer.neededFor(req)) {
+                    noi.offer.onNetworkUnneeded(req);
+                }
+            }
+        }
+    }
+
+    private void handleRemoveNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) {
+        for (final NetworkRequestInfo nri : nris) {
+            if (mDefaultRequest == nri) {
+                // Make sure we never remove the default request.
+                continue;
+            }
+            handleRemoveNetworkRequest(nri);
+        }
+    }
+
+    private void removeListenRequestFromNetworks(@NonNull final NetworkRequest req) {
+        // listens don't have a singular affected Network. Check all networks to see
+        // if this listen request applies and remove it.
+        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+            nai.removeRequest(req.requestId);
+            if (req.networkCapabilities.hasSignalStrength()
+                    && nai.satisfiesImmutableCapabilitiesOf(req)) {
+                updateSignalStrengthThresholds(nai, "RELEASE", req);
+            }
+        }
+    }
+
+    /**
+     * Remove a NetworkRequestInfo's satisfied request from its 'satisfier' (NetworkAgentInfo) and
+     * manage the necessary upkeep (linger, teardown networks, etc.) when doing so.
+     * @param nri the NetworkRequestInfo to disassociate from its current NetworkAgentInfo
+     */
+    private void removeSatisfiedNetworkRequestFromNetwork(@NonNull final NetworkRequestInfo nri) {
+        boolean wasKept = false;
+        final NetworkAgentInfo nai = nri.getSatisfier();
+        if (nai != null) {
+            final int requestLegacyType = nri.getActiveRequest().legacyType;
+            final boolean wasBackgroundNetwork = nai.isBackgroundNetwork();
+            nai.removeRequest(nri.getActiveRequest().requestId);
+            if (VDBG || DDBG) {
+                log(" Removing from current network " + nai.toShortString()
+                        + ", leaving " + nai.numNetworkRequests() + " requests.");
+            }
+            // If there are still lingered requests on this network, don't tear it down,
+            // but resume lingering instead.
+            final long now = SystemClock.elapsedRealtime();
+            if (updateInactivityState(nai, now)) {
+                notifyNetworkLosing(nai, now);
+            }
+            if (unneeded(nai, UnneededFor.TEARDOWN)) {
+                if (DBG) log("no live requests for " + nai.toShortString() + "; disconnecting");
+                teardownUnneededNetwork(nai);
+            } else {
+                wasKept = true;
+            }
+            nri.setSatisfier(null, null);
+            if (!wasBackgroundNetwork && nai.isBackgroundNetwork()) {
+                // Went from foreground to background.
+                updateCapabilitiesForNetwork(nai);
+            }
+
+            // Maintain the illusion.  When this request arrived, we might have pretended
+            // that a network connected to serve it, even though the network was already
+            // connected.  Now that this request has gone away, we might have to pretend
+            // that the network disconnected.  LegacyTypeTracker will generate that
+            // phantom disconnect for this type.
+            if (requestLegacyType != TYPE_NONE) {
+                boolean doRemove = true;
+                if (wasKept) {
+                    // check if any of the remaining requests for this network are for the
+                    // same legacy type - if so, don't remove the nai
+                    for (int i = 0; i < nai.numNetworkRequests(); i++) {
+                        NetworkRequest otherRequest = nai.requestAt(i);
+                        if (otherRequest.legacyType == requestLegacyType
+                                && otherRequest.isRequest()) {
+                            if (DBG) log(" still have other legacy request - leaving");
+                            doRemove = false;
+                        }
+                    }
+                }
+
+                if (doRemove) {
+                    mLegacyTypeTracker.remove(requestLegacyType, nai, false);
+                }
+            }
+        }
+    }
+
+    private PerUidCounter getRequestCounter(NetworkRequestInfo nri) {
+        return checkAnyPermissionOf(
+                nri.mPid, nri.mUid, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+                ? mSystemNetworkRequestCounter : mNetworkRequestCounter;
+    }
+
+    @Override
+    public void setAcceptUnvalidated(Network network, boolean accept, boolean always) {
+        enforceNetworkStackSettingsOrSetup();
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_ACCEPT_UNVALIDATED,
+                encodeBool(accept), encodeBool(always), network));
+    }
+
+    @Override
+    public void setAcceptPartialConnectivity(Network network, boolean accept, boolean always) {
+        enforceNetworkStackSettingsOrSetup();
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_ACCEPT_PARTIAL_CONNECTIVITY,
+                encodeBool(accept), encodeBool(always), network));
+    }
+
+    @Override
+    public void setAvoidUnvalidated(Network network) {
+        enforceNetworkStackSettingsOrSetup();
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_AVOID_UNVALIDATED, network));
+    }
+
+    private void handleSetAcceptUnvalidated(Network network, boolean accept, boolean always) {
+        if (DBG) log("handleSetAcceptUnvalidated network=" + network +
+                " accept=" + accept + " always=" + always);
+
+        NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+        if (nai == null) {
+            // Nothing to do.
+            return;
+        }
+
+        if (nai.everValidated) {
+            // The network validated while the dialog box was up. Take no action.
+            return;
+        }
+
+        if (!nai.networkAgentConfig.explicitlySelected) {
+            Log.wtf(TAG, "BUG: setAcceptUnvalidated non non-explicitly selected network");
+        }
+
+        if (accept != nai.networkAgentConfig.acceptUnvalidated) {
+            nai.networkAgentConfig.acceptUnvalidated = accept;
+            // If network becomes partial connectivity and user already accepted to use this
+            // network, we should respect the user's option and don't need to popup the
+            // PARTIAL_CONNECTIVITY notification to user again.
+            nai.networkAgentConfig.acceptPartialConnectivity = accept;
+            nai.updateScoreForNetworkAgentUpdate();
+            rematchAllNetworksAndRequests();
+        }
+
+        if (always) {
+            nai.onSaveAcceptUnvalidated(accept);
+        }
+
+        if (!accept) {
+            // Tell the NetworkAgent to not automatically reconnect to the network.
+            nai.onPreventAutomaticReconnect();
+            // Teardown the network.
+            teardownUnneededNetwork(nai);
+        }
+
+    }
+
+    private void handleSetAcceptPartialConnectivity(Network network, boolean accept,
+            boolean always) {
+        if (DBG) {
+            log("handleSetAcceptPartialConnectivity network=" + network + " accept=" + accept
+                    + " always=" + always);
+        }
+
+        final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+        if (nai == null) {
+            // Nothing to do.
+            return;
+        }
+
+        if (nai.lastValidated) {
+            // The network validated while the dialog box was up. Take no action.
+            return;
+        }
+
+        if (accept != nai.networkAgentConfig.acceptPartialConnectivity) {
+            nai.networkAgentConfig.acceptPartialConnectivity = accept;
+        }
+
+        // TODO: Use the current design or save the user choice into IpMemoryStore.
+        if (always) {
+            nai.onSaveAcceptUnvalidated(accept);
+        }
+
+        if (!accept) {
+            // Tell the NetworkAgent to not automatically reconnect to the network.
+            nai.onPreventAutomaticReconnect();
+            // Tear down the network.
+            teardownUnneededNetwork(nai);
+        } else {
+            // Inform NetworkMonitor that partial connectivity is acceptable. This will likely
+            // result in a partial connectivity result which will be processed by
+            // maybeHandleNetworkMonitorMessage.
+            //
+            // TODO: NetworkMonitor does not refer to the "never ask again" bit. The bit is stored
+            // per network. Therefore, NetworkMonitor may still do https probe.
+            nai.networkMonitor().setAcceptPartialConnectivity();
+        }
+    }
+
+    private void handleSetAvoidUnvalidated(Network network) {
+        NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+        if (nai == null || nai.lastValidated) {
+            // Nothing to do. The network either disconnected or revalidated.
+            return;
+        }
+        if (!nai.avoidUnvalidated) {
+            nai.avoidUnvalidated = true;
+            nai.updateScoreForNetworkAgentUpdate();
+            rematchAllNetworksAndRequests();
+        }
+    }
+
+    private void scheduleUnvalidatedPrompt(NetworkAgentInfo nai) {
+        if (VDBG) log("scheduleUnvalidatedPrompt " + nai.network);
+        mHandler.sendMessageDelayed(
+                mHandler.obtainMessage(EVENT_PROMPT_UNVALIDATED, nai.network),
+                PROMPT_UNVALIDATED_DELAY_MS);
+    }
+
+    @Override
+    public void startCaptivePortalApp(Network network) {
+        enforceNetworkStackOrSettingsPermission();
+        mHandler.post(() -> {
+            NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+            if (nai == null) return;
+            if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) return;
+            nai.networkMonitor().launchCaptivePortalApp();
+        });
+    }
+
+    /**
+     * NetworkStack endpoint to start the captive portal app. The NetworkStack needs to use this
+     * endpoint as it does not have INTERACT_ACROSS_USERS_FULL itself.
+     * @param network Network on which the captive portal was detected.
+     * @param appExtras Bundle to use as intent extras for the captive portal application.
+     *                  Must be treated as opaque to avoid preventing the captive portal app to
+     *                  update its arguments.
+     */
+    @Override
+    public void startCaptivePortalAppInternal(Network network, Bundle appExtras) {
+        mContext.enforceCallingOrSelfPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                "ConnectivityService");
+
+        final Intent appIntent = new Intent(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
+        appIntent.putExtras(appExtras);
+        appIntent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL,
+                new CaptivePortal(new CaptivePortalImpl(network).asBinder()));
+        appIntent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mContext.startActivityAsUser(appIntent, UserHandle.CURRENT);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    private class CaptivePortalImpl extends ICaptivePortal.Stub {
+        private final Network mNetwork;
+
+        private CaptivePortalImpl(Network network) {
+            mNetwork = network;
+        }
+
+        @Override
+        public void appResponse(final int response) {
+            if (response == CaptivePortal.APP_RETURN_WANTED_AS_IS) {
+                enforceSettingsPermission();
+            }
+
+            final NetworkMonitorManager nm = getNetworkMonitorManager(mNetwork);
+            if (nm == null) return;
+            nm.notifyCaptivePortalAppFinished(response);
+        }
+
+        @Override
+        public void appRequest(final int request) {
+            final NetworkMonitorManager nm = getNetworkMonitorManager(mNetwork);
+            if (nm == null) return;
+
+            if (request == CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED) {
+                checkNetworkStackPermission();
+                nm.forceReevaluation(mDeps.getCallingUid());
+            }
+        }
+
+        @Nullable
+        private NetworkMonitorManager getNetworkMonitorManager(final Network network) {
+            // getNetworkAgentInfoForNetwork is thread-safe
+            final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+            if (nai == null) return null;
+
+            // nai.networkMonitor() is thread-safe
+            return nai.networkMonitor();
+        }
+    }
+
+    public boolean avoidBadWifi() {
+        return mMultinetworkPolicyTracker.getAvoidBadWifi();
+    }
+
+    /**
+     * Return whether the device should maintain continuous, working connectivity by switching away
+     * from WiFi networks having no connectivity.
+     * @see MultinetworkPolicyTracker#getAvoidBadWifi()
+     */
+    public boolean shouldAvoidBadWifi() {
+        if (!checkNetworkStackPermission()) {
+            throw new SecurityException("avoidBadWifi requires NETWORK_STACK permission");
+        }
+        return avoidBadWifi();
+    }
+
+    private void updateAvoidBadWifi() {
+        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+            nai.updateScoreForNetworkAgentUpdate();
+        }
+        rematchAllNetworksAndRequests();
+    }
+
+    // TODO: Evaluate whether this is of interest to other consumers of
+    // MultinetworkPolicyTracker and worth moving out of here.
+    private void dumpAvoidBadWifiSettings(IndentingPrintWriter pw) {
+        final boolean configRestrict = mMultinetworkPolicyTracker.configRestrictsAvoidBadWifi();
+        if (!configRestrict) {
+            pw.println("Bad Wi-Fi avoidance: unrestricted");
+            return;
+        }
+
+        pw.println("Bad Wi-Fi avoidance: " + avoidBadWifi());
+        pw.increaseIndent();
+        pw.println("Config restrict:   " + configRestrict);
+
+        final String value = mMultinetworkPolicyTracker.getAvoidBadWifiSetting();
+        String description;
+        // Can't use a switch statement because strings are legal case labels, but null is not.
+        if ("0".equals(value)) {
+            description = "get stuck";
+        } else if (value == null) {
+            description = "prompt";
+        } else if ("1".equals(value)) {
+            description = "avoid";
+        } else {
+            description = value + " (?)";
+        }
+        pw.println("User setting:      " + description);
+        pw.println("Network overrides:");
+        pw.increaseIndent();
+        for (NetworkAgentInfo nai : networksSortedById()) {
+            if (nai.avoidUnvalidated) {
+                pw.println(nai.toShortString());
+            }
+        }
+        pw.decreaseIndent();
+        pw.decreaseIndent();
+    }
+
+    // TODO: This method is copied from TetheringNotificationUpdater. Should have a utility class to
+    // unify the method.
+    private static @NonNull String getSettingsPackageName(@NonNull final PackageManager pm) {
+        final Intent settingsIntent = new Intent(Settings.ACTION_SETTINGS);
+        final ComponentName settingsComponent = settingsIntent.resolveActivity(pm);
+        return settingsComponent != null
+                ? settingsComponent.getPackageName() : "com.android.settings";
+    }
+
+    private void showNetworkNotification(NetworkAgentInfo nai, NotificationType type) {
+        final String action;
+        final boolean highPriority;
+        switch (type) {
+            case NO_INTERNET:
+                action = ConnectivityManager.ACTION_PROMPT_UNVALIDATED;
+                // High priority because it is only displayed for explicitly selected networks.
+                highPriority = true;
+                break;
+            case PRIVATE_DNS_BROKEN:
+                action = Settings.ACTION_WIRELESS_SETTINGS;
+                // High priority because we should let user know why there is no internet.
+                highPriority = true;
+                break;
+            case LOST_INTERNET:
+                action = ConnectivityManager.ACTION_PROMPT_LOST_VALIDATION;
+                // High priority because it could help the user avoid unexpected data usage.
+                highPriority = true;
+                break;
+            case PARTIAL_CONNECTIVITY:
+                action = ConnectivityManager.ACTION_PROMPT_PARTIAL_CONNECTIVITY;
+                // Don't bother the user with a high-priority notification if the network was not
+                // explicitly selected by the user.
+                highPriority = nai.networkAgentConfig.explicitlySelected;
+                break;
+            default:
+                Log.wtf(TAG, "Unknown notification type " + type);
+                return;
+        }
+
+        Intent intent = new Intent(action);
+        if (type != NotificationType.PRIVATE_DNS_BROKEN) {
+            intent.putExtra(ConnectivityManager.EXTRA_NETWORK, nai.network);
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            // Some OEMs have their own Settings package. Thus, need to get the current using
+            // Settings package name instead of just use default name "com.android.settings".
+            final String settingsPkgName = getSettingsPackageName(mContext.getPackageManager());
+            intent.setClassName(settingsPkgName,
+                    settingsPkgName + ".wifi.WifiNoInternetDialog");
+        }
+
+        PendingIntent pendingIntent = PendingIntent.getActivity(
+                mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
+                0 /* requestCode */,
+                intent,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+        mNotifier.showNotification(
+                nai.network.getNetId(), type, nai, null, pendingIntent, highPriority);
+    }
+
+    private boolean shouldPromptUnvalidated(NetworkAgentInfo nai) {
+        // Don't prompt if the network is validated, and don't prompt on captive portals
+        // because we're already prompting the user to sign in.
+        if (nai.everValidated || nai.everCaptivePortalDetected) {
+            return false;
+        }
+
+        // If a network has partial connectivity, always prompt unless the user has already accepted
+        // partial connectivity and selected don't ask again. This ensures that if the device
+        // automatically connects to a network that has partial Internet access, the user will
+        // always be able to use it, either because they've already chosen "don't ask again" or
+        // because we have prompt them.
+        if (nai.partialConnectivity && !nai.networkAgentConfig.acceptPartialConnectivity) {
+            return true;
+        }
+
+        // If a network has no Internet access, only prompt if the network was explicitly selected
+        // and if the user has not already told us to use the network regardless of whether it
+        // validated or not.
+        if (nai.networkAgentConfig.explicitlySelected
+                && !nai.networkAgentConfig.acceptUnvalidated) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private void handlePromptUnvalidated(Network network) {
+        if (VDBG || DDBG) log("handlePromptUnvalidated " + network);
+        NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+
+        if (nai == null || !shouldPromptUnvalidated(nai)) {
+            return;
+        }
+
+        // Stop automatically reconnecting to this network in the future. Automatically connecting
+        // to a network that provides no or limited connectivity is not useful, because the user
+        // cannot use that network except through the notification shown by this method, and the
+        // notification is only shown if the network is explicitly selected by the user.
+        nai.onPreventAutomaticReconnect();
+
+        // TODO: Evaluate if it's needed to wait 8 seconds for triggering notification when
+        // NetworkMonitor detects the network is partial connectivity. Need to change the design to
+        // popup the notification immediately when the network is partial connectivity.
+        if (nai.partialConnectivity) {
+            showNetworkNotification(nai, NotificationType.PARTIAL_CONNECTIVITY);
+        } else {
+            showNetworkNotification(nai, NotificationType.NO_INTERNET);
+        }
+    }
+
+    private void handleNetworkUnvalidated(NetworkAgentInfo nai) {
+        NetworkCapabilities nc = nai.networkCapabilities;
+        if (DBG) log("handleNetworkUnvalidated " + nai.toShortString() + " cap=" + nc);
+
+        if (!nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+            return;
+        }
+
+        if (mMultinetworkPolicyTracker.shouldNotifyWifiUnvalidated()) {
+            showNetworkNotification(nai, NotificationType.LOST_INTERNET);
+        }
+    }
+
+    @Override
+    public int getMultipathPreference(Network network) {
+        enforceAccessPermission();
+
+        NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+        if (nai != null && nai.networkCapabilities
+                .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) {
+            return ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED;
+        }
+
+        final NetworkPolicyManager netPolicyManager =
+                 mContext.getSystemService(NetworkPolicyManager.class);
+
+        final long token = Binder.clearCallingIdentity();
+        final int networkPreference;
+        try {
+            networkPreference = netPolicyManager.getMultipathPreference(network);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+        if (networkPreference != 0) {
+            return networkPreference;
+        }
+        return mMultinetworkPolicyTracker.getMeteredMultipathPreference();
+    }
+
+    @Override
+    public NetworkRequest getDefaultRequest() {
+        return mDefaultRequest.mRequests.get(0);
+    }
+
+    private class InternalHandler extends Handler {
+        public InternalHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_EXPIRE_NET_TRANSITION_WAKELOCK:
+                case EVENT_CLEAR_NET_TRANSITION_WAKELOCK: {
+                    handleReleaseNetworkTransitionWakelock(msg.what);
+                    break;
+                }
+                case EVENT_APPLY_GLOBAL_HTTP_PROXY: {
+                    mProxyTracker.loadDeprecatedGlobalHttpProxy();
+                    break;
+                }
+                case EVENT_PROXY_HAS_CHANGED: {
+                    final Pair<Network, ProxyInfo> arg = (Pair<Network, ProxyInfo>) msg.obj;
+                    handleApplyDefaultProxy(arg.second);
+                    break;
+                }
+                case EVENT_REGISTER_NETWORK_PROVIDER: {
+                    handleRegisterNetworkProvider((NetworkProviderInfo) msg.obj);
+                    break;
+                }
+                case EVENT_UNREGISTER_NETWORK_PROVIDER: {
+                    handleUnregisterNetworkProvider((Messenger) msg.obj);
+                    break;
+                }
+                case EVENT_REGISTER_NETWORK_OFFER: {
+                    handleRegisterNetworkOffer((NetworkOffer) msg.obj);
+                    break;
+                }
+                case EVENT_UNREGISTER_NETWORK_OFFER: {
+                    final NetworkOfferInfo offer =
+                            findNetworkOfferInfoByCallback((INetworkOfferCallback) msg.obj);
+                    if (null != offer) {
+                        handleUnregisterNetworkOffer(offer);
+                    }
+                    break;
+                }
+                case EVENT_REGISTER_NETWORK_AGENT: {
+                    final Pair<NetworkAgentInfo, INetworkMonitor> arg =
+                            (Pair<NetworkAgentInfo, INetworkMonitor>) msg.obj;
+                    handleRegisterNetworkAgent(arg.first, arg.second);
+                    break;
+                }
+                case EVENT_REGISTER_NETWORK_REQUEST:
+                case EVENT_REGISTER_NETWORK_LISTENER: {
+                    handleRegisterNetworkRequest((NetworkRequestInfo) msg.obj);
+                    break;
+                }
+                case EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT:
+                case EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT: {
+                    handleRegisterNetworkRequestWithIntent(msg);
+                    break;
+                }
+                case EVENT_TIMEOUT_NETWORK_REQUEST: {
+                    NetworkRequestInfo nri = (NetworkRequestInfo) msg.obj;
+                    handleTimedOutNetworkRequest(nri);
+                    break;
+                }
+                case EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT: {
+                    handleReleaseNetworkRequestWithIntent((PendingIntent) msg.obj, msg.arg1);
+                    break;
+                }
+                case EVENT_RELEASE_NETWORK_REQUEST: {
+                    handleReleaseNetworkRequest((NetworkRequest) msg.obj, msg.arg1,
+                            /* callOnUnavailable */ false);
+                    break;
+                }
+                case EVENT_SET_ACCEPT_UNVALIDATED: {
+                    Network network = (Network) msg.obj;
+                    handleSetAcceptUnvalidated(network, toBool(msg.arg1), toBool(msg.arg2));
+                    break;
+                }
+                case EVENT_SET_ACCEPT_PARTIAL_CONNECTIVITY: {
+                    Network network = (Network) msg.obj;
+                    handleSetAcceptPartialConnectivity(network, toBool(msg.arg1),
+                            toBool(msg.arg2));
+                    break;
+                }
+                case EVENT_SET_AVOID_UNVALIDATED: {
+                    handleSetAvoidUnvalidated((Network) msg.obj);
+                    break;
+                }
+                case EVENT_PROMPT_UNVALIDATED: {
+                    handlePromptUnvalidated((Network) msg.obj);
+                    break;
+                }
+                case EVENT_CONFIGURE_ALWAYS_ON_NETWORKS: {
+                    handleConfigureAlwaysOnNetworks();
+                    break;
+                }
+                // Sent by KeepaliveTracker to process an app request on the state machine thread.
+                case NetworkAgent.CMD_START_SOCKET_KEEPALIVE: {
+                    mKeepaliveTracker.handleStartKeepalive(msg);
+                    break;
+                }
+                // Sent by KeepaliveTracker to process an app request on the state machine thread.
+                case NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE: {
+                    NetworkAgentInfo nai = getNetworkAgentInfoForNetwork((Network) msg.obj);
+                    int slot = msg.arg1;
+                    int reason = msg.arg2;
+                    mKeepaliveTracker.handleStopKeepalive(nai, slot, reason);
+                    break;
+                }
+                case EVENT_REVALIDATE_NETWORK: {
+                    handleReportNetworkConnectivity((Network) msg.obj, msg.arg1, toBool(msg.arg2));
+                    break;
+                }
+                case EVENT_PRIVATE_DNS_SETTINGS_CHANGED:
+                    handlePrivateDnsSettingsChanged();
+                    break;
+                case EVENT_PRIVATE_DNS_VALIDATION_UPDATE:
+                    handlePrivateDnsValidationUpdate(
+                            (PrivateDnsValidationUpdate) msg.obj);
+                    break;
+                case EVENT_UID_BLOCKED_REASON_CHANGED:
+                    handleUidBlockedReasonChanged(msg.arg1, msg.arg2);
+                    break;
+                case EVENT_SET_REQUIRE_VPN_FOR_UIDS:
+                    handleSetRequireVpnForUids(toBool(msg.arg1), (UidRange[]) msg.obj);
+                    break;
+                case EVENT_SET_OEM_NETWORK_PREFERENCE: {
+                    final Pair<OemNetworkPreferences, IOnCompleteListener> arg =
+                            (Pair<OemNetworkPreferences, IOnCompleteListener>) msg.obj;
+                    handleSetOemNetworkPreference(arg.first, arg.second);
+                    break;
+                }
+                case EVENT_SET_PROFILE_NETWORK_PREFERENCE: {
+                    final Pair<ProfileNetworkPreferences.Preference, IOnCompleteListener> arg =
+                            (Pair<ProfileNetworkPreferences.Preference, IOnCompleteListener>)
+                                    msg.obj;
+                    handleSetProfileNetworkPreference(arg.first, arg.second);
+                    break;
+                }
+                case EVENT_REPORT_NETWORK_ACTIVITY:
+                    mNetworkActivityTracker.handleReportNetworkActivity();
+                    break;
+                case EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED:
+                    handleMobileDataPreferredUidsChanged();
+                    break;
+            }
+        }
+    }
+
+    @Override
+    @Deprecated
+    public int getLastTetherError(String iface) {
+        final TetheringManager tm = (TetheringManager) mContext.getSystemService(
+                Context.TETHERING_SERVICE);
+        return tm.getLastTetherError(iface);
+    }
+
+    @Override
+    @Deprecated
+    public String[] getTetherableIfaces() {
+        final TetheringManager tm = (TetheringManager) mContext.getSystemService(
+                Context.TETHERING_SERVICE);
+        return tm.getTetherableIfaces();
+    }
+
+    @Override
+    @Deprecated
+    public String[] getTetheredIfaces() {
+        final TetheringManager tm = (TetheringManager) mContext.getSystemService(
+                Context.TETHERING_SERVICE);
+        return tm.getTetheredIfaces();
+    }
+
+
+    @Override
+    @Deprecated
+    public String[] getTetheringErroredIfaces() {
+        final TetheringManager tm = (TetheringManager) mContext.getSystemService(
+                Context.TETHERING_SERVICE);
+
+        return tm.getTetheringErroredIfaces();
+    }
+
+    @Override
+    @Deprecated
+    public String[] getTetherableUsbRegexs() {
+        final TetheringManager tm = (TetheringManager) mContext.getSystemService(
+                Context.TETHERING_SERVICE);
+
+        return tm.getTetherableUsbRegexs();
+    }
+
+    @Override
+    @Deprecated
+    public String[] getTetherableWifiRegexs() {
+        final TetheringManager tm = (TetheringManager) mContext.getSystemService(
+                Context.TETHERING_SERVICE);
+        return tm.getTetherableWifiRegexs();
+    }
+
+    // Called when we lose the default network and have no replacement yet.
+    // This will automatically be cleared after X seconds or a new default network
+    // becomes CONNECTED, whichever happens first.  The timer is started by the
+    // first caller and not restarted by subsequent callers.
+    private void ensureNetworkTransitionWakelock(String forWhom) {
+        synchronized (this) {
+            if (mNetTransitionWakeLock.isHeld()) {
+                return;
+            }
+            mNetTransitionWakeLock.acquire();
+            mLastWakeLockAcquireTimestamp = SystemClock.elapsedRealtime();
+            mTotalWakelockAcquisitions++;
+        }
+        mWakelockLogs.log("ACQUIRE for " + forWhom);
+        Message msg = mHandler.obtainMessage(EVENT_EXPIRE_NET_TRANSITION_WAKELOCK);
+        final int lockTimeout = mResources.get().getInteger(
+                R.integer.config_networkTransitionTimeout);
+        mHandler.sendMessageDelayed(msg, lockTimeout);
+    }
+
+    // Called when we gain a new default network to release the network transition wakelock in a
+    // second, to allow a grace period for apps to reconnect over the new network. Pending expiry
+    // message is cancelled.
+    private void scheduleReleaseNetworkTransitionWakelock() {
+        synchronized (this) {
+            if (!mNetTransitionWakeLock.isHeld()) {
+                return; // expiry message released the lock first.
+            }
+        }
+        // Cancel self timeout on wakelock hold.
+        mHandler.removeMessages(EVENT_EXPIRE_NET_TRANSITION_WAKELOCK);
+        Message msg = mHandler.obtainMessage(EVENT_CLEAR_NET_TRANSITION_WAKELOCK);
+        mHandler.sendMessageDelayed(msg, 1000);
+    }
+
+    // Called when either message of ensureNetworkTransitionWakelock or
+    // scheduleReleaseNetworkTransitionWakelock is processed.
+    private void handleReleaseNetworkTransitionWakelock(int eventId) {
+        String event = eventName(eventId);
+        synchronized (this) {
+            if (!mNetTransitionWakeLock.isHeld()) {
+                mWakelockLogs.log(String.format("RELEASE: already released (%s)", event));
+                Log.w(TAG, "expected Net Transition WakeLock to be held");
+                return;
+            }
+            mNetTransitionWakeLock.release();
+            long lockDuration = SystemClock.elapsedRealtime() - mLastWakeLockAcquireTimestamp;
+            mTotalWakelockDurationMs += lockDuration;
+            mMaxWakelockDurationMs = Math.max(mMaxWakelockDurationMs, lockDuration);
+            mTotalWakelockReleases++;
+        }
+        mWakelockLogs.log(String.format("RELEASE (%s)", event));
+    }
+
+    // 100 percent is full good, 0 is full bad.
+    @Override
+    public void reportInetCondition(int networkType, int percentage) {
+        NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+        if (nai == null) return;
+        reportNetworkConnectivity(nai.network, percentage > 50);
+    }
+
+    @Override
+    public void reportNetworkConnectivity(Network network, boolean hasConnectivity) {
+        enforceAccessPermission();
+        enforceInternetPermission();
+        final int uid = mDeps.getCallingUid();
+        final int connectivityInfo = encodeBool(hasConnectivity);
+
+        // Handle ConnectivityDiagnostics event before attempting to revalidate the network. This
+        // forces an ordering of ConnectivityDiagnostics events in the case where hasConnectivity
+        // does not match the known connectivity of the network - this causes NetworkMonitor to
+        // revalidate the network and generate a ConnectivityDiagnostics ConnectivityReport event.
+        final NetworkAgentInfo nai;
+        if (network == null) {
+            nai = getDefaultNetwork();
+        } else {
+            nai = getNetworkAgentInfoForNetwork(network);
+        }
+        if (nai != null) {
+            mConnectivityDiagnosticsHandler.sendMessage(
+                    mConnectivityDiagnosticsHandler.obtainMessage(
+                            ConnectivityDiagnosticsHandler.EVENT_NETWORK_CONNECTIVITY_REPORTED,
+                            connectivityInfo, 0, nai));
+        }
+
+        mHandler.sendMessage(
+                mHandler.obtainMessage(EVENT_REVALIDATE_NETWORK, uid, connectivityInfo, network));
+    }
+
+    private void handleReportNetworkConnectivity(
+            Network network, int uid, boolean hasConnectivity) {
+        final NetworkAgentInfo nai;
+        if (network == null) {
+            nai = getDefaultNetwork();
+        } else {
+            nai = getNetworkAgentInfoForNetwork(network);
+        }
+        if (nai == null || nai.networkInfo.getState() == NetworkInfo.State.DISCONNECTING ||
+            nai.networkInfo.getState() == NetworkInfo.State.DISCONNECTED) {
+            return;
+        }
+        // Revalidate if the app report does not match our current validated state.
+        if (hasConnectivity == nai.lastValidated) {
+            return;
+        }
+        if (DBG) {
+            int netid = nai.network.getNetId();
+            log("reportNetworkConnectivity(" + netid + ", " + hasConnectivity + ") by " + uid);
+        }
+        // Validating a network that has not yet connected could result in a call to
+        // rematchNetworkAndRequests() which is not meant to work on such networks.
+        if (!nai.everConnected) {
+            return;
+        }
+        final NetworkCapabilities nc = getNetworkCapabilitiesInternal(nai);
+        if (isNetworkWithCapabilitiesBlocked(nc, uid, false)) {
+            return;
+        }
+        nai.networkMonitor().forceReevaluation(uid);
+    }
+
+    // TODO: call into netd.
+    private boolean queryUserAccess(int uid, Network network) {
+        final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+        if (nai == null) return false;
+
+        // Any UID can use its default network.
+        if (nai == getDefaultNetworkForUid(uid)) return true;
+
+        // Privileged apps can use any network.
+        if (mPermissionMonitor.hasRestrictedNetworksPermission(uid)) {
+            return true;
+        }
+
+        // An unprivileged UID can use a VPN iff the VPN applies to it.
+        if (nai.isVPN()) {
+            return nai.networkCapabilities.appliesToUid(uid);
+        }
+
+        // An unprivileged UID can bypass the VPN that applies to it only if it can protect its
+        // sockets, i.e., if it is the owner.
+        final NetworkAgentInfo vpn = getVpnForUid(uid);
+        if (vpn != null && !vpn.networkAgentConfig.allowBypass
+                && uid != vpn.networkCapabilities.getOwnerUid()) {
+            return false;
+        }
+
+        // The UID's permission must be at least sufficient for the network. Since the restricted
+        // permission was already checked above, that just leaves background networks.
+        if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_FOREGROUND)) {
+            return mPermissionMonitor.hasUseBackgroundNetworksPermission(uid);
+        }
+
+        // Unrestricted network. Anyone gets to use it.
+        return true;
+    }
+
+    /**
+     * Returns information about the proxy a certain network is using. If given a null network, it
+     * it will return the proxy for the bound network for the caller app or the default proxy if
+     * none.
+     *
+     * @param network the network we want to get the proxy information for.
+     * @return Proxy information if a network has a proxy configured, or otherwise null.
+     */
+    @Override
+    public ProxyInfo getProxyForNetwork(Network network) {
+        final ProxyInfo globalProxy = mProxyTracker.getGlobalProxy();
+        if (globalProxy != null) return globalProxy;
+        if (network == null) {
+            // Get the network associated with the calling UID.
+            final Network activeNetwork = getActiveNetworkForUidInternal(mDeps.getCallingUid(),
+                    true);
+            if (activeNetwork == null) {
+                return null;
+            }
+            return getLinkPropertiesProxyInfo(activeNetwork);
+        } else if (mDeps.queryUserAccess(mDeps.getCallingUid(), network, this)) {
+            // Don't call getLinkProperties() as it requires ACCESS_NETWORK_STATE permission, which
+            // caller may not have.
+            return getLinkPropertiesProxyInfo(network);
+        }
+        // No proxy info available if the calling UID does not have network access.
+        return null;
+    }
+
+
+    private ProxyInfo getLinkPropertiesProxyInfo(Network network) {
+        final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+        if (nai == null) return null;
+        synchronized (nai) {
+            final ProxyInfo linkHttpProxy = nai.linkProperties.getHttpProxy();
+            return linkHttpProxy == null ? null : new ProxyInfo(linkHttpProxy);
+        }
+    }
+
+    @Override
+    public void setGlobalProxy(@Nullable final ProxyInfo proxyProperties) {
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+        mProxyTracker.setGlobalProxy(proxyProperties);
+    }
+
+    @Override
+    @Nullable
+    public ProxyInfo getGlobalProxy() {
+        return mProxyTracker.getGlobalProxy();
+    }
+
+    private void handleApplyDefaultProxy(ProxyInfo proxy) {
+        if (proxy != null && TextUtils.isEmpty(proxy.getHost())
+                && Uri.EMPTY.equals(proxy.getPacFileUrl())) {
+            proxy = null;
+        }
+        mProxyTracker.setDefaultProxy(proxy);
+    }
+
+    // If the proxy has changed from oldLp to newLp, resend proxy broadcast. This method gets called
+    // when any network changes proxy.
+    // TODO: Remove usage of broadcast extras as they are deprecated and not applicable in a
+    // multi-network world where an app might be bound to a non-default network.
+    private void updateProxy(LinkProperties newLp, LinkProperties oldLp) {
+        ProxyInfo newProxyInfo = newLp == null ? null : newLp.getHttpProxy();
+        ProxyInfo oldProxyInfo = oldLp == null ? null : oldLp.getHttpProxy();
+
+        if (!ProxyTracker.proxyInfoEqual(newProxyInfo, oldProxyInfo)) {
+            mProxyTracker.sendProxyBroadcast();
+        }
+    }
+
+    private static class SettingsObserver extends ContentObserver {
+        final private HashMap<Uri, Integer> mUriEventMap;
+        final private Context mContext;
+        final private Handler mHandler;
+
+        SettingsObserver(Context context, Handler handler) {
+            super(null);
+            mUriEventMap = new HashMap<>();
+            mContext = context;
+            mHandler = handler;
+        }
+
+        void observe(Uri uri, int what) {
+            mUriEventMap.put(uri, what);
+            final ContentResolver resolver = mContext.getContentResolver();
+            resolver.registerContentObserver(uri, false, this);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            Log.wtf(TAG, "Should never be reached.");
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            final Integer what = mUriEventMap.get(uri);
+            if (what != null) {
+                mHandler.obtainMessage(what).sendToTarget();
+            } else {
+                loge("No matching event to send for URI=" + uri);
+            }
+        }
+    }
+
+    private static void log(String s) {
+        Log.d(TAG, s);
+    }
+
+    private static void logw(String s) {
+        Log.w(TAG, s);
+    }
+
+    private static void logwtf(String s) {
+        Log.wtf(TAG, s);
+    }
+
+    private static void logwtf(String s, Throwable t) {
+        Log.wtf(TAG, s, t);
+    }
+
+    private static void loge(String s) {
+        Log.e(TAG, s);
+    }
+
+    private static void loge(String s, Throwable t) {
+        Log.e(TAG, s, t);
+    }
+
+    /**
+     * Return the information of all ongoing VPNs.
+     *
+     * <p>This method is used to update NetworkStatsService.
+     *
+     * <p>Must be called on the handler thread.
+     */
+    private UnderlyingNetworkInfo[] getAllVpnInfo() {
+        ensureRunningOnConnectivityServiceThread();
+        if (mLockdownEnabled) {
+            return new UnderlyingNetworkInfo[0];
+        }
+        List<UnderlyingNetworkInfo> infoList = new ArrayList<>();
+        for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+            UnderlyingNetworkInfo info = createVpnInfo(nai);
+            if (info != null) {
+                infoList.add(info);
+            }
+        }
+        return infoList.toArray(new UnderlyingNetworkInfo[infoList.size()]);
+    }
+
+    /**
+     * @return VPN information for accounting, or null if we can't retrieve all required
+     *         information, e.g underlying ifaces.
+     */
+    private UnderlyingNetworkInfo createVpnInfo(NetworkAgentInfo nai) {
+        if (!nai.isVPN()) return null;
+
+        Network[] underlyingNetworks = nai.declaredUnderlyingNetworks;
+        // see VpnService.setUnderlyingNetworks()'s javadoc about how to interpret
+        // the underlyingNetworks list.
+        if (underlyingNetworks == null) {
+            final NetworkAgentInfo defaultNai = getDefaultNetworkForUid(
+                    nai.networkCapabilities.getOwnerUid());
+            if (defaultNai != null) {
+                underlyingNetworks = new Network[] { defaultNai.network };
+            }
+        }
+
+        if (CollectionUtils.isEmpty(underlyingNetworks)) return null;
+
+        List<String> interfaces = new ArrayList<>();
+        for (Network network : underlyingNetworks) {
+            NetworkAgentInfo underlyingNai = getNetworkAgentInfoForNetwork(network);
+            if (underlyingNai == null) continue;
+            LinkProperties lp = underlyingNai.linkProperties;
+            for (String iface : lp.getAllInterfaceNames()) {
+                if (!TextUtils.isEmpty(iface)) {
+                    interfaces.add(iface);
+                }
+            }
+        }
+
+        if (interfaces.isEmpty()) return null;
+
+        // Must be non-null or NetworkStatsService will crash.
+        // Cannot happen in production code because Vpn only registers the NetworkAgent after the
+        // tun or ipsec interface is created.
+        // TODO: Remove this check.
+        if (nai.linkProperties.getInterfaceName() == null) return null;
+
+        return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(),
+                nai.linkProperties.getInterfaceName(), interfaces);
+    }
+
+    // TODO This needs to be the default network that applies to the NAI.
+    private Network[] underlyingNetworksOrDefault(final int ownerUid,
+            Network[] underlyingNetworks) {
+        final Network defaultNetwork = getNetwork(getDefaultNetworkForUid(ownerUid));
+        if (underlyingNetworks == null && defaultNetwork != null) {
+            // null underlying networks means to track the default.
+            underlyingNetworks = new Network[] { defaultNetwork };
+        }
+        return underlyingNetworks;
+    }
+
+    // Returns true iff |network| is an underlying network of |nai|.
+    private boolean hasUnderlyingNetwork(NetworkAgentInfo nai, Network network) {
+        // TODO: support more than one level of underlying networks, either via a fixed-depth search
+        // (e.g., 2 levels of underlying networks), or via loop detection, or....
+        if (!nai.supportsUnderlyingNetworks()) return false;
+        final Network[] underlying = underlyingNetworksOrDefault(
+                nai.networkCapabilities.getOwnerUid(), nai.declaredUnderlyingNetworks);
+        return CollectionUtils.contains(underlying, network);
+    }
+
+    /**
+     * Recompute the capabilities for any networks that had a specific network as underlying.
+     *
+     * When underlying networks change, such networks may have to update capabilities to reflect
+     * things like the metered bit, their transports, and so on. The capabilities are calculated
+     * immediately. This method runs on the ConnectivityService thread.
+     */
+    private void propagateUnderlyingNetworkCapabilities(Network updatedNetwork) {
+        ensureRunningOnConnectivityServiceThread();
+        for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+            if (updatedNetwork == null || hasUnderlyingNetwork(nai, updatedNetwork)) {
+                updateCapabilitiesForNetwork(nai);
+            }
+        }
+    }
+
+    private boolean isUidBlockedByVpn(int uid, List<UidRange> blockedUidRanges) {
+        // Determine whether this UID is blocked because of always-on VPN lockdown. If a VPN applies
+        // to the UID, then the UID is not blocked because always-on VPN lockdown applies only when
+        // a VPN is not up.
+        final NetworkAgentInfo vpnNai = getVpnForUid(uid);
+        if (vpnNai != null && !vpnNai.networkAgentConfig.allowBypass) return false;
+        for (UidRange range : blockedUidRanges) {
+            if (range.contains(uid)) return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void setRequireVpnForUids(boolean requireVpn, UidRange[] ranges) {
+        enforceNetworkStackOrSettingsPermission();
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_REQUIRE_VPN_FOR_UIDS,
+                encodeBool(requireVpn), 0 /* arg2 */, ranges));
+    }
+
+    private void handleSetRequireVpnForUids(boolean requireVpn, UidRange[] ranges) {
+        if (DBG) {
+            Log.d(TAG, "Setting VPN " + (requireVpn ? "" : "not ") + "required for UIDs: "
+                    + Arrays.toString(ranges));
+        }
+        // Cannot use a Set since the list of UID ranges might contain duplicates.
+        final List<UidRange> newVpnBlockedUidRanges = new ArrayList(mVpnBlockedUidRanges);
+        for (int i = 0; i < ranges.length; i++) {
+            if (requireVpn) {
+                newVpnBlockedUidRanges.add(ranges[i]);
+            } else {
+                newVpnBlockedUidRanges.remove(ranges[i]);
+            }
+        }
+
+        try {
+            mNetd.networkRejectNonSecureVpn(requireVpn, toUidRangeStableParcels(ranges));
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "setRequireVpnForUids(" + requireVpn + ", "
+                    + Arrays.toString(ranges) + "): netd command failed: " + e);
+        }
+
+        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+            final boolean curMetered = nai.networkCapabilities.isMetered();
+            maybeNotifyNetworkBlocked(nai, curMetered, curMetered,
+                    mVpnBlockedUidRanges, newVpnBlockedUidRanges);
+        }
+
+        mVpnBlockedUidRanges = newVpnBlockedUidRanges;
+    }
+
+    @Override
+    public void setLegacyLockdownVpnEnabled(boolean enabled) {
+        enforceNetworkStackOrSettingsPermission();
+        mHandler.post(() -> mLockdownEnabled = enabled);
+    }
+
+    private boolean isLegacyLockdownNai(NetworkAgentInfo nai) {
+        return mLockdownEnabled
+                && getVpnType(nai) == VpnManager.TYPE_VPN_LEGACY
+                && nai.networkCapabilities.appliesToUid(Process.FIRST_APPLICATION_UID);
+    }
+
+    private NetworkAgentInfo getLegacyLockdownNai() {
+        if (!mLockdownEnabled) {
+            return null;
+        }
+        // The legacy lockdown VPN always only applies to userId 0.
+        final NetworkAgentInfo nai = getVpnForUid(Process.FIRST_APPLICATION_UID);
+        if (nai == null || !isLegacyLockdownNai(nai)) return null;
+
+        // The legacy lockdown VPN must always have exactly one underlying network.
+        // This code may run on any thread and declaredUnderlyingNetworks may change, so store it in
+        // a local variable. There is no need to make a copy because its contents cannot change.
+        final Network[] underlying = nai.declaredUnderlyingNetworks;
+        if (underlying == null ||  underlying.length != 1) {
+            return null;
+        }
+
+        // The legacy lockdown VPN always uses the default network.
+        // If the VPN's underlying network is no longer the current default network, it means that
+        // the default network has just switched, and the VPN is about to disconnect.
+        // Report that the VPN is not connected, so the state of NetworkInfo objects overwritten
+        // by filterForLegacyLockdown will be set to CONNECTING and not CONNECTED.
+        final NetworkAgentInfo defaultNetwork = getDefaultNetwork();
+        if (defaultNetwork == null || !defaultNetwork.network.equals(underlying[0])) {
+            return null;
+        }
+
+        return nai;
+    };
+
+    // TODO: move all callers to filterForLegacyLockdown and delete this method.
+    // This likely requires making sendLegacyNetworkBroadcast take a NetworkInfo object instead of
+    // just a DetailedState object.
+    private DetailedState getLegacyLockdownState(DetailedState origState) {
+        if (origState != DetailedState.CONNECTED) {
+            return origState;
+        }
+        return (mLockdownEnabled && getLegacyLockdownNai() == null)
+                ? DetailedState.CONNECTING
+                : DetailedState.CONNECTED;
+    }
+
+    private void filterForLegacyLockdown(NetworkInfo ni) {
+        if (!mLockdownEnabled || !ni.isConnected()) return;
+        // The legacy lockdown VPN replaces the state of every network in CONNECTED state with the
+        // state of its VPN. This is to ensure that when an underlying network connects, apps will
+        // not see a CONNECTIVITY_ACTION broadcast for a network in state CONNECTED until the VPN
+        // comes up, at which point there is a new CONNECTIVITY_ACTION broadcast for the underlying
+        // network, this time with a state of CONNECTED.
+        //
+        // Now that the legacy lockdown code lives in ConnectivityService, and no longer has access
+        // to the internal state of the Vpn object, always replace the state with CONNECTING. This
+        // is not too far off the truth, since an always-on VPN, when not connected, is always
+        // trying to reconnect.
+        if (getLegacyLockdownNai() == null) {
+            ni.setDetailedState(DetailedState.CONNECTING, "", null);
+        }
+    }
+
+    @Override
+    public void setProvisioningNotificationVisible(boolean visible, int networkType,
+            String action) {
+        enforceSettingsPermission();
+        if (!ConnectivityManager.isNetworkTypeValid(networkType)) {
+            return;
+        }
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            // Concatenate the range of types onto the range of NetIDs.
+            int id = NetIdManager.MAX_NET_ID + 1 + (networkType - ConnectivityManager.TYPE_NONE);
+            mNotifier.setProvNotificationVisible(visible, id, action);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    @Override
+    public void setAirplaneMode(boolean enable) {
+        enforceAirplaneModePermission();
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            final ContentResolver cr = mContext.getContentResolver();
+            Settings.Global.putInt(cr, Settings.Global.AIRPLANE_MODE_ON, encodeBool(enable));
+            Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+            intent.putExtra("state", enable);
+            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private void onUserAdded(@NonNull final UserHandle user) {
+        mPermissionMonitor.onUserAdded(user);
+        if (mOemNetworkPreferences.getNetworkPreferences().size() > 0) {
+            handleSetOemNetworkPreference(mOemNetworkPreferences, null);
+        }
+    }
+
+    private void onUserRemoved(@NonNull final UserHandle user) {
+        mPermissionMonitor.onUserRemoved(user);
+        // If there was a network preference for this user, remove it.
+        handleSetProfileNetworkPreference(new ProfileNetworkPreferences.Preference(user, null),
+                null /* listener */);
+        if (mOemNetworkPreferences.getNetworkPreferences().size() > 0) {
+            handleSetOemNetworkPreference(mOemNetworkPreferences, null);
+        }
+    }
+
+    private void onPackageChanged(@NonNull final String packageName) {
+        // This is necessary in case a package is added or removed, but also when it's replaced to
+        // run as a new UID by its manifest rules. Also, if a separate package shares the same UID
+        // as one in the preferences, then it should follow the same routing as that other package,
+        // which means updating the rules is never to be needed in this case (whether it joins or
+        // leaves a UID with a preference).
+        if (isMappedInOemNetworkPreference(packageName)) {
+            handleSetOemNetworkPreference(mOemNetworkPreferences, null);
+        }
+    }
+
+    private final BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            ensureRunningOnConnectivityServiceThread();
+            final String action = intent.getAction();
+            final UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
+
+            // User should be filled for below intents, check the existence.
+            if (user == null) {
+                Log.wtf(TAG, intent.getAction() + " broadcast without EXTRA_USER");
+                return;
+            }
+
+            if (Intent.ACTION_USER_ADDED.equals(action)) {
+                onUserAdded(user);
+            } else if (Intent.ACTION_USER_REMOVED.equals(action)) {
+                onUserRemoved(user);
+            }  else {
+                Log.wtf(TAG, "received unexpected intent: " + action);
+            }
+        }
+    };
+
+    private final BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            ensureRunningOnConnectivityServiceThread();
+            switch (intent.getAction()) {
+                case Intent.ACTION_PACKAGE_ADDED:
+                case Intent.ACTION_PACKAGE_REMOVED:
+                case Intent.ACTION_PACKAGE_REPLACED:
+                    onPackageChanged(intent.getData().getSchemeSpecificPart());
+                    break;
+                default:
+                    Log.wtf(TAG, "received unexpected intent: " + intent.getAction());
+            }
+        }
+    };
+
+    private final HashMap<Messenger, NetworkProviderInfo> mNetworkProviderInfos = new HashMap<>();
+    private final HashMap<NetworkRequest, NetworkRequestInfo> mNetworkRequests = new HashMap<>();
+
+    private static class NetworkProviderInfo {
+        public final String name;
+        public final Messenger messenger;
+        private final IBinder.DeathRecipient mDeathRecipient;
+        public final int providerId;
+
+        NetworkProviderInfo(String name, Messenger messenger, int providerId,
+                @NonNull IBinder.DeathRecipient deathRecipient) {
+            this.name = name;
+            this.messenger = messenger;
+            this.providerId = providerId;
+            mDeathRecipient = deathRecipient;
+
+            if (mDeathRecipient == null) {
+                throw new AssertionError("Must pass a deathRecipient");
+            }
+        }
+
+        void connect(Context context, Handler handler) {
+            try {
+                messenger.getBinder().linkToDeath(mDeathRecipient, 0);
+            } catch (RemoteException e) {
+                mDeathRecipient.binderDied();
+            }
+        }
+    }
+
+    private void ensureAllNetworkRequestsHaveType(List<NetworkRequest> requests) {
+        for (int i = 0; i < requests.size(); i++) {
+            ensureNetworkRequestHasType(requests.get(i));
+        }
+    }
+
+    private void ensureNetworkRequestHasType(NetworkRequest request) {
+        if (request.type == NetworkRequest.Type.NONE) {
+            throw new IllegalArgumentException(
+                    "All NetworkRequests in ConnectivityService must have a type");
+        }
+    }
+
+    /**
+     * Tracks info about the requester.
+     * Also used to notice when the calling process dies so as to self-expire
+     */
+    @VisibleForTesting
+    protected class NetworkRequestInfo implements IBinder.DeathRecipient {
+        // The requests to be satisfied in priority order. Non-multilayer requests will only have a
+        // single NetworkRequest in mRequests.
+        final List<NetworkRequest> mRequests;
+
+        // mSatisfier and mActiveRequest rely on one another therefore set them together.
+        void setSatisfier(
+                @Nullable final NetworkAgentInfo satisfier,
+                @Nullable final NetworkRequest activeRequest) {
+            mSatisfier = satisfier;
+            mActiveRequest = activeRequest;
+        }
+
+        // The network currently satisfying this NRI. Only one request in an NRI can have a
+        // satisfier. For non-multilayer requests, only non-listen requests can have a satisfier.
+        @Nullable
+        private NetworkAgentInfo mSatisfier;
+        NetworkAgentInfo getSatisfier() {
+            return mSatisfier;
+        }
+
+        // The request in mRequests assigned to a network agent. This is null if none of the
+        // requests in mRequests can be satisfied. This member has the constraint of only being
+        // accessible on the handler thread.
+        @Nullable
+        private NetworkRequest mActiveRequest;
+        NetworkRequest getActiveRequest() {
+            return mActiveRequest;
+        }
+
+        final PendingIntent mPendingIntent;
+        boolean mPendingIntentSent;
+        @Nullable
+        final Messenger mMessenger;
+
+        // Information about the caller that caused this object to be created.
+        @Nullable
+        private final IBinder mBinder;
+        final int mPid;
+        final int mUid;
+        final @NetworkCallback.Flag int mCallbackFlags;
+        @Nullable
+        final String mCallingAttributionTag;
+
+        // Counter keeping track of this NRI.
+        final PerUidCounter mPerUidCounter;
+
+        // Effective UID of this request. This is different from mUid when a privileged process
+        // files a request on behalf of another UID. This UID is used to determine blocked status,
+        // UID matching, and so on. mUid above is used for permission checks and to enforce the
+        // maximum limit of registered callbacks per UID.
+        final int mAsUid;
+
+        // Default network priority of this request.
+        private final int mDefaultNetworkPriority;
+
+        int getDefaultNetworkPriority() {
+            return mDefaultNetworkPriority;
+        }
+
+        // In order to preserve the mapping of NetworkRequest-to-callback when apps register
+        // callbacks using a returned NetworkRequest, the original NetworkRequest needs to be
+        // maintained for keying off of. This is only a concern when the original nri
+        // mNetworkRequests changes which happens currently for apps that register callbacks to
+        // track the default network. In those cases, the nri is updated to have mNetworkRequests
+        // that match the per-app default nri that currently tracks the calling app's uid so that
+        // callbacks are fired at the appropriate time. When the callbacks fire,
+        // mNetworkRequestForCallback will be used so as to preserve the caller's mapping. When
+        // callbacks are updated to key off of an nri vs NetworkRequest, this stops being an issue.
+        // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
+        @NonNull
+        private final NetworkRequest mNetworkRequestForCallback;
+        NetworkRequest getNetworkRequestForCallback() {
+            return mNetworkRequestForCallback;
+        }
+
+        /**
+         * Get the list of UIDs this nri applies to.
+         */
+        @NonNull
+        Set<UidRange> getUids() {
+            // networkCapabilities.getUids() returns a defensive copy.
+            // multilayer requests will all have the same uids so return the first one.
+            final Set<UidRange> uids = mRequests.get(0).networkCapabilities.getUidRanges();
+            return (null == uids) ? new ArraySet<>() : uids;
+        }
+
+        NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r,
+                @Nullable final PendingIntent pi, @Nullable String callingAttributionTag) {
+            this(asUid, Collections.singletonList(r), r, pi, callingAttributionTag,
+                    DEFAULT_NETWORK_PRIORITY_NONE);
+        }
+
+        NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
+                @NonNull final NetworkRequest requestForCallback, @Nullable final PendingIntent pi,
+                @Nullable String callingAttributionTag, final int defaultNetworkPriority) {
+            ensureAllNetworkRequestsHaveType(r);
+            mRequests = initializeRequests(r);
+            mNetworkRequestForCallback = requestForCallback;
+            mPendingIntent = pi;
+            mMessenger = null;
+            mBinder = null;
+            mPid = getCallingPid();
+            mUid = mDeps.getCallingUid();
+            mAsUid = asUid;
+            mPerUidCounter = getRequestCounter(this);
+            mPerUidCounter.incrementCountOrThrow(mUid);
+            /**
+             * Location sensitive data not included in pending intent. Only included in
+             * {@link NetworkCallback}.
+             */
+            mCallbackFlags = NetworkCallback.FLAG_NONE;
+            mCallingAttributionTag = callingAttributionTag;
+            mDefaultNetworkPriority = defaultNetworkPriority;
+        }
+
+        NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r, @Nullable final Messenger m,
+                @Nullable final IBinder binder,
+                @NetworkCallback.Flag int callbackFlags,
+                @Nullable String callingAttributionTag) {
+            this(asUid, Collections.singletonList(r), r, m, binder, callbackFlags,
+                    callingAttributionTag);
+        }
+
+        NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
+                @NonNull final NetworkRequest requestForCallback, @Nullable final Messenger m,
+                @Nullable final IBinder binder,
+                @NetworkCallback.Flag int callbackFlags,
+                @Nullable String callingAttributionTag) {
+            super();
+            ensureAllNetworkRequestsHaveType(r);
+            mRequests = initializeRequests(r);
+            mNetworkRequestForCallback = requestForCallback;
+            mMessenger = m;
+            mBinder = binder;
+            mPid = getCallingPid();
+            mUid = mDeps.getCallingUid();
+            mAsUid = asUid;
+            mPendingIntent = null;
+            mPerUidCounter = getRequestCounter(this);
+            mPerUidCounter.incrementCountOrThrow(mUid);
+            mCallbackFlags = callbackFlags;
+            mCallingAttributionTag = callingAttributionTag;
+            mDefaultNetworkPriority = DEFAULT_NETWORK_PRIORITY_NONE;
+            linkDeathRecipient();
+        }
+
+        NetworkRequestInfo(@NonNull final NetworkRequestInfo nri,
+                @NonNull final List<NetworkRequest> r) {
+            super();
+            ensureAllNetworkRequestsHaveType(r);
+            mRequests = initializeRequests(r);
+            mNetworkRequestForCallback = nri.getNetworkRequestForCallback();
+            final NetworkAgentInfo satisfier = nri.getSatisfier();
+            if (null != satisfier) {
+                // If the old NRI was satisfied by an NAI, then it may have had an active request.
+                // The active request is necessary to figure out what callbacks to send, in
+                // particular then a network updates its capabilities.
+                // As this code creates a new NRI with a new set of requests, figure out which of
+                // the list of requests should be the active request. It is always the first
+                // request of the list that can be satisfied by the satisfier since the order of
+                // requests is a priority order.
+                // Note even in the presence of a satisfier there may not be an active request,
+                // when the satisfier is the no-service network.
+                NetworkRequest activeRequest = null;
+                for (final NetworkRequest candidate : r) {
+                    if (candidate.canBeSatisfiedBy(satisfier.networkCapabilities)) {
+                        activeRequest = candidate;
+                        break;
+                    }
+                }
+                setSatisfier(satisfier, activeRequest);
+            }
+            mMessenger = nri.mMessenger;
+            mBinder = nri.mBinder;
+            mPid = nri.mPid;
+            mUid = nri.mUid;
+            mAsUid = nri.mAsUid;
+            mPendingIntent = nri.mPendingIntent;
+            mPerUidCounter = getRequestCounter(this);
+            mPerUidCounter.incrementCountOrThrow(mUid);
+            mCallbackFlags = nri.mCallbackFlags;
+            mCallingAttributionTag = nri.mCallingAttributionTag;
+            mDefaultNetworkPriority = DEFAULT_NETWORK_PRIORITY_NONE;
+            linkDeathRecipient();
+        }
+
+        NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r) {
+            this(asUid, Collections.singletonList(r), DEFAULT_NETWORK_PRIORITY_NONE);
+        }
+
+        NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
+                final int defaultNetworkPriority) {
+            this(asUid, r, r.get(0), null /* pi */, null /* callingAttributionTag */,
+                    defaultNetworkPriority);
+        }
+
+        // True if this NRI is being satisfied. It also accounts for if the nri has its satisifer
+        // set to the mNoServiceNetwork in which case mActiveRequest will be null thus returning
+        // false.
+        boolean isBeingSatisfied() {
+            return (null != mSatisfier && null != mActiveRequest);
+        }
+
+        boolean isMultilayerRequest() {
+            return mRequests.size() > 1;
+        }
+
+        private List<NetworkRequest> initializeRequests(List<NetworkRequest> r) {
+            // Creating a defensive copy to prevent the sender from modifying the list being
+            // reflected in the return value of this method.
+            final List<NetworkRequest> tempRequests = new ArrayList<>(r);
+            return Collections.unmodifiableList(tempRequests);
+        }
+
+        void decrementRequestCount() {
+            mPerUidCounter.decrementCount(mUid);
+        }
+
+        void linkDeathRecipient() {
+            if (null != mBinder) {
+                try {
+                    mBinder.linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    binderDied();
+                }
+            }
+        }
+
+        void unlinkDeathRecipient() {
+            if (null != mBinder) {
+                mBinder.unlinkToDeath(this, 0);
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            log("ConnectivityService NetworkRequestInfo binderDied(" +
+                    mRequests + ", " + mBinder + ")");
+            releaseNetworkRequests(mRequests);
+        }
+
+        @Override
+        public String toString() {
+            final String asUidString = (mAsUid == mUid) ? "" : " asUid: " + mAsUid;
+            return "uid/pid:" + mUid + "/" + mPid + asUidString + " activeRequest: "
+                    + (mActiveRequest == null ? null : mActiveRequest.requestId)
+                    + " callbackRequest: "
+                    + mNetworkRequestForCallback.requestId
+                    + " " + mRequests
+                    + (mPendingIntent == null ? "" : " to trigger " + mPendingIntent)
+                    + " callback flags: " + mCallbackFlags;
+        }
+    }
+
+    private void ensureRequestableCapabilities(NetworkCapabilities networkCapabilities) {
+        final String badCapability = networkCapabilities.describeFirstNonRequestableCapability();
+        if (badCapability != null) {
+            throw new IllegalArgumentException("Cannot request network with " + badCapability);
+        }
+    }
+
+    // This checks that the passed capabilities either do not request a
+    // specific SSID/SignalStrength, or the calling app has permission to do so.
+    private void ensureSufficientPermissionsForRequest(NetworkCapabilities nc,
+            int callerPid, int callerUid, String callerPackageName) {
+        if (null != nc.getSsid() && !checkSettingsPermission(callerPid, callerUid)) {
+            throw new SecurityException("Insufficient permissions to request a specific SSID");
+        }
+
+        if (nc.hasSignalStrength()
+                && !checkNetworkSignalStrengthWakeupPermission(callerPid, callerUid)) {
+            throw new SecurityException(
+                    "Insufficient permissions to request a specific signal strength");
+        }
+        mAppOpsManager.checkPackage(callerUid, callerPackageName);
+
+        if (!nc.getSubscriptionIds().isEmpty()) {
+            enforceNetworkFactoryPermission();
+        }
+    }
+
+    private int[] getSignalStrengthThresholds(@NonNull final NetworkAgentInfo nai) {
+        final SortedSet<Integer> thresholds = new TreeSet<>();
+        synchronized (nai) {
+            // mNetworkRequests may contain the same value multiple times in case of
+            // multilayer requests. It won't matter in this case because the thresholds
+            // will then be the same and be deduplicated as they enter the `thresholds` set.
+            // TODO : have mNetworkRequests be a Set<NetworkRequestInfo> or the like.
+            for (final NetworkRequestInfo nri : mNetworkRequests.values()) {
+                for (final NetworkRequest req : nri.mRequests) {
+                    if (req.networkCapabilities.hasSignalStrength()
+                            && nai.satisfiesImmutableCapabilitiesOf(req)) {
+                        thresholds.add(req.networkCapabilities.getSignalStrength());
+                    }
+                }
+            }
+        }
+        return CollectionUtils.toIntArray(new ArrayList<>(thresholds));
+    }
+
+    private void updateSignalStrengthThresholds(
+            NetworkAgentInfo nai, String reason, NetworkRequest request) {
+        final int[] thresholdsArray = getSignalStrengthThresholds(nai);
+
+        if (VDBG || (DBG && !"CONNECT".equals(reason))) {
+            String detail;
+            if (request != null && request.networkCapabilities.hasSignalStrength()) {
+                detail = reason + " " + request.networkCapabilities.getSignalStrength();
+            } else {
+                detail = reason;
+            }
+            log(String.format("updateSignalStrengthThresholds: %s, sending %s to %s",
+                    detail, Arrays.toString(thresholdsArray), nai.toShortString()));
+        }
+
+        nai.onSignalStrengthThresholdsUpdated(thresholdsArray);
+    }
+
+    private void ensureValidNetworkSpecifier(NetworkCapabilities nc) {
+        if (nc == null) {
+            return;
+        }
+        NetworkSpecifier ns = nc.getNetworkSpecifier();
+        if (ns == null) {
+            return;
+        }
+        if (ns instanceof MatchAllNetworkSpecifier) {
+            throw new IllegalArgumentException("A MatchAllNetworkSpecifier is not permitted");
+        }
+    }
+
+    private void ensureValid(NetworkCapabilities nc) {
+        ensureValidNetworkSpecifier(nc);
+        if (nc.isPrivateDnsBroken()) {
+            throw new IllegalArgumentException("Can't request broken private DNS");
+        }
+    }
+
+    private boolean isTargetSdkAtleast(int version, int callingUid,
+            @NonNull String callingPackageName) {
+        final UserHandle user = UserHandle.getUserHandleForUid(callingUid);
+        final PackageManager pm =
+                mContext.createContextAsUser(user, 0 /* flags */).getPackageManager();
+        try {
+            final int callingVersion = pm.getTargetSdkVersion(callingPackageName);
+            if (callingVersion < version) return false;
+        } catch (PackageManager.NameNotFoundException e) { }
+        return true;
+    }
+
+    @Override
+    public NetworkRequest requestNetwork(int asUid, NetworkCapabilities networkCapabilities,
+            int reqTypeInt, Messenger messenger, int timeoutMs, IBinder binder,
+            int legacyType, int callbackFlags, @NonNull String callingPackageName,
+            @Nullable String callingAttributionTag) {
+        if (legacyType != TYPE_NONE && !checkNetworkStackPermission()) {
+            if (isTargetSdkAtleast(Build.VERSION_CODES.M, mDeps.getCallingUid(),
+                    callingPackageName)) {
+                throw new SecurityException("Insufficient permissions to specify legacy type");
+            }
+        }
+        final NetworkCapabilities defaultNc = mDefaultRequest.mRequests.get(0).networkCapabilities;
+        final int callingUid = mDeps.getCallingUid();
+        // Privileged callers can track the default network of another UID by passing in a UID.
+        if (asUid != Process.INVALID_UID) {
+            enforceSettingsPermission();
+        } else {
+            asUid = callingUid;
+        }
+        final NetworkRequest.Type reqType;
+        try {
+            reqType = NetworkRequest.Type.values()[reqTypeInt];
+        } catch (ArrayIndexOutOfBoundsException e) {
+            throw new IllegalArgumentException("Unsupported request type " + reqTypeInt);
+        }
+        switch (reqType) {
+            case TRACK_DEFAULT:
+                // If the request type is TRACK_DEFAULT, the passed {@code networkCapabilities}
+                // is unused and will be replaced by ones appropriate for the UID (usually, the
+                // calling app). This allows callers to keep track of the default network.
+                networkCapabilities = copyDefaultNetworkCapabilitiesForUid(
+                        defaultNc, asUid, callingUid, callingPackageName);
+                enforceAccessPermission();
+                break;
+            case TRACK_SYSTEM_DEFAULT:
+                enforceSettingsPermission();
+                networkCapabilities = new NetworkCapabilities(defaultNc);
+                break;
+            case BACKGROUND_REQUEST:
+                enforceNetworkStackOrSettingsPermission();
+                // Fall-through since other checks are the same with normal requests.
+            case REQUEST:
+                networkCapabilities = new NetworkCapabilities(networkCapabilities);
+                enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
+                        callingAttributionTag);
+                // 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
+                enforceMeteredApnPolicy(networkCapabilities);
+                break;
+            case LISTEN_FOR_BEST:
+                enforceAccessPermission();
+                networkCapabilities = new NetworkCapabilities(networkCapabilities);
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported request type " + reqType);
+        }
+        ensureRequestableCapabilities(networkCapabilities);
+        ensureSufficientPermissionsForRequest(networkCapabilities,
+                Binder.getCallingPid(), callingUid, callingPackageName);
+
+        // Enforce FOREGROUND if the caller does not have permission to use background network.
+        if (reqType == LISTEN_FOR_BEST) {
+            restrictBackgroundRequestForCaller(networkCapabilities);
+        }
+
+        // Set the UID range for this request to the single UID of the requester, unless the
+        // requester has the permission to specify other UIDs.
+        // This will overwrite any allowed UIDs in the requested capabilities. Though there
+        // are no visible methods to set the UIDs, an app could use reflection to try and get
+        // networks for other apps so it's essential that the UIDs are overwritten.
+        // Also set the requester UID and package name in the request.
+        restrictRequestUidsForCallerAndSetRequestorInfo(networkCapabilities,
+                callingUid, callingPackageName);
+
+        if (timeoutMs < 0) {
+            throw new IllegalArgumentException("Bad timeout specified");
+        }
+        ensureValid(networkCapabilities);
+
+        final NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, legacyType,
+                nextNetworkRequestId(), reqType);
+        final NetworkRequestInfo nri = getNriToRegister(
+                asUid, networkRequest, messenger, binder, callbackFlags,
+                callingAttributionTag);
+        if (DBG) log("requestNetwork for " + nri);
+
+        // For TRACK_SYSTEM_DEFAULT callbacks, the capabilities have been modified since they were
+        // copied from the default request above. (This is necessary to ensure, for example, that
+        // the callback does not leak sensitive information to unprivileged apps.) Check that the
+        // changes don't alter request matching.
+        if (reqType == NetworkRequest.Type.TRACK_SYSTEM_DEFAULT &&
+                (!networkCapabilities.equalRequestableCapabilities(defaultNc))) {
+            throw new IllegalStateException(
+                    "TRACK_SYSTEM_DEFAULT capabilities don't match default request: "
+                    + networkCapabilities + " vs. " + defaultNc);
+        }
+
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST, nri));
+        if (timeoutMs > 0) {
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_TIMEOUT_NETWORK_REQUEST,
+                    nri), timeoutMs);
+        }
+        return networkRequest;
+    }
+
+    /**
+     * Return the nri to be used when registering a network request. Specifically, this is used with
+     * requests registered to track the default request. If there is currently a per-app default
+     * tracking the app requestor, then we need to create a version of this nri that mirrors that of
+     * the tracking per-app default so that callbacks are sent to the app requestor appropriately.
+     * @param asUid the uid on behalf of which to file the request. Different from requestorUid
+     *              when a privileged caller is tracking the default network for another uid.
+     * @param nr the network request for the nri.
+     * @param msgr the messenger for the nri.
+     * @param binder the binder for the nri.
+     * @param callingAttributionTag the calling attribution tag for the nri.
+     * @return the nri to register.
+     */
+    private NetworkRequestInfo getNriToRegister(final int asUid, @NonNull final NetworkRequest nr,
+            @Nullable final Messenger msgr, @Nullable final IBinder binder,
+            @NetworkCallback.Flag int callbackFlags,
+            @Nullable String callingAttributionTag) {
+        final List<NetworkRequest> requests;
+        if (NetworkRequest.Type.TRACK_DEFAULT == nr.type) {
+            requests = copyDefaultNetworkRequestsForUid(
+                    asUid, nr.getRequestorUid(), nr.getRequestorPackageName());
+        } else {
+            requests = Collections.singletonList(nr);
+        }
+        return new NetworkRequestInfo(
+                asUid, requests, nr, msgr, binder, callbackFlags, callingAttributionTag);
+    }
+
+    private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities,
+            String callingPackageName, String callingAttributionTag) {
+        if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) {
+            enforceConnectivityRestrictedNetworksPermission();
+        } else {
+            enforceChangePermission(callingPackageName, callingAttributionTag);
+        }
+    }
+
+    @Override
+    public boolean requestBandwidthUpdate(Network network) {
+        enforceAccessPermission();
+        NetworkAgentInfo nai = null;
+        if (network == null) {
+            return false;
+        }
+        synchronized (mNetworkForNetId) {
+            nai = mNetworkForNetId.get(network.getNetId());
+        }
+        if (nai != null) {
+            nai.onBandwidthUpdateRequested();
+            synchronized (mBandwidthRequests) {
+                final int uid = mDeps.getCallingUid();
+                Integer uidReqs = mBandwidthRequests.get(uid);
+                if (uidReqs == null) {
+                    uidReqs = 0;
+                }
+                mBandwidthRequests.put(uid, ++uidReqs);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isSystem(int uid) {
+        return uid < Process.FIRST_APPLICATION_UID;
+    }
+
+    private void enforceMeteredApnPolicy(NetworkCapabilities networkCapabilities) {
+        final int uid = mDeps.getCallingUid();
+        if (isSystem(uid)) {
+            // Exemption for system uid.
+            return;
+        }
+        if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED)) {
+            // Policy already enforced.
+            return;
+        }
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {
+                // If UID is restricted, don't allow them to bring up metered APNs.
+                networkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    @Override
+    public NetworkRequest pendingRequestForNetwork(NetworkCapabilities networkCapabilities,
+            PendingIntent operation, @NonNull String callingPackageName,
+            @Nullable String callingAttributionTag) {
+        Objects.requireNonNull(operation, "PendingIntent cannot be null.");
+        final int callingUid = mDeps.getCallingUid();
+        networkCapabilities = new NetworkCapabilities(networkCapabilities);
+        enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
+                callingAttributionTag);
+        enforceMeteredApnPolicy(networkCapabilities);
+        ensureRequestableCapabilities(networkCapabilities);
+        ensureSufficientPermissionsForRequest(networkCapabilities,
+                Binder.getCallingPid(), callingUid, callingPackageName);
+        ensureValidNetworkSpecifier(networkCapabilities);
+        restrictRequestUidsForCallerAndSetRequestorInfo(networkCapabilities,
+                callingUid, callingPackageName);
+
+        NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, TYPE_NONE,
+                nextNetworkRequestId(), NetworkRequest.Type.REQUEST);
+        NetworkRequestInfo nri = new NetworkRequestInfo(callingUid, networkRequest, operation,
+                callingAttributionTag);
+        if (DBG) log("pendingRequest for " + nri);
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT,
+                nri));
+        return networkRequest;
+    }
+
+    private void releasePendingNetworkRequestWithDelay(PendingIntent operation) {
+        mHandler.sendMessageDelayed(
+                mHandler.obtainMessage(EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT,
+                mDeps.getCallingUid(), 0, operation), mReleasePendingIntentDelayMs);
+    }
+
+    @Override
+    public void releasePendingNetworkRequest(PendingIntent operation) {
+        Objects.requireNonNull(operation, "PendingIntent cannot be null.");
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT,
+                mDeps.getCallingUid(), 0, operation));
+    }
+
+    // In order to implement the compatibility measure for pre-M apps that call
+    // WifiManager.enableNetwork(..., true) without also binding to that network explicitly,
+    // WifiManager registers a network listen for the purpose of calling setProcessDefaultNetwork.
+    // This ensures it has permission to do so.
+    private boolean hasWifiNetworkListenPermission(NetworkCapabilities nc) {
+        if (nc == null) {
+            return false;
+        }
+        int[] transportTypes = nc.getTransportTypes();
+        if (transportTypes.length != 1 || transportTypes[0] != NetworkCapabilities.TRANSPORT_WIFI) {
+            return false;
+        }
+        try {
+            mContext.enforceCallingOrSelfPermission(
+                    android.Manifest.permission.ACCESS_WIFI_STATE,
+                    "ConnectivityService");
+        } catch (SecurityException e) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public NetworkRequest listenForNetwork(NetworkCapabilities networkCapabilities,
+            Messenger messenger, IBinder binder,
+            @NetworkCallback.Flag int callbackFlags,
+            @NonNull String callingPackageName, @NonNull String callingAttributionTag) {
+        final int callingUid = mDeps.getCallingUid();
+        if (!hasWifiNetworkListenPermission(networkCapabilities)) {
+            enforceAccessPermission();
+        }
+
+        NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
+        ensureSufficientPermissionsForRequest(networkCapabilities,
+                Binder.getCallingPid(), callingUid, callingPackageName);
+        restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+        // Apps without the CHANGE_NETWORK_STATE permission can't use background networks, so
+        // make all their listens include NET_CAPABILITY_FOREGROUND. That way, they will get
+        // onLost and onAvailable callbacks when networks move in and out of the background.
+        // There is no need to do this for requests because an app without CHANGE_NETWORK_STATE
+        // can't request networks.
+        restrictBackgroundRequestForCaller(nc);
+        ensureValid(nc);
+
+        NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(),
+                NetworkRequest.Type.LISTEN);
+        NetworkRequestInfo nri =
+                new NetworkRequestInfo(callingUid, networkRequest, messenger, binder, callbackFlags,
+                        callingAttributionTag);
+        if (VDBG) log("listenForNetwork for " + nri);
+
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri));
+        return networkRequest;
+    }
+
+    @Override
+    public void pendingListenForNetwork(NetworkCapabilities networkCapabilities,
+            PendingIntent operation, @NonNull String callingPackageName,
+            @Nullable String callingAttributionTag) {
+        Objects.requireNonNull(operation, "PendingIntent cannot be null.");
+        final int callingUid = mDeps.getCallingUid();
+        if (!hasWifiNetworkListenPermission(networkCapabilities)) {
+            enforceAccessPermission();
+        }
+        ensureValid(networkCapabilities);
+        ensureSufficientPermissionsForRequest(networkCapabilities,
+                Binder.getCallingPid(), callingUid, callingPackageName);
+        final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
+        restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+
+        NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(),
+                NetworkRequest.Type.LISTEN);
+        NetworkRequestInfo nri = new NetworkRequestInfo(callingUid, networkRequest, operation,
+                callingAttributionTag);
+        if (VDBG) log("pendingListenForNetwork for " + nri);
+
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri));
+    }
+
+    /** Returns the next Network provider ID. */
+    public final int nextNetworkProviderId() {
+        return mNextNetworkProviderId.getAndIncrement();
+    }
+
+    private void releaseNetworkRequests(List<NetworkRequest> networkRequests) {
+        for (int i = 0; i < networkRequests.size(); i++) {
+            releaseNetworkRequest(networkRequests.get(i));
+        }
+    }
+
+    @Override
+    public void releaseNetworkRequest(NetworkRequest networkRequest) {
+        ensureNetworkRequestHasType(networkRequest);
+        mHandler.sendMessage(mHandler.obtainMessage(
+                EVENT_RELEASE_NETWORK_REQUEST, 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.
+            // This will never happen if an app calls ConnectivityManager#registerNetworkProvider,
+            // as that will throw if a duplicate provider is registered.
+            loge("Attempt to register existing NetworkProviderInfo "
+                    + mNetworkProviderInfos.get(npi.messenger).name);
+            return;
+        }
+
+        if (DBG) log("Got NetworkProvider Messenger for " + npi.name);
+        mNetworkProviderInfos.put(npi.messenger, npi);
+        npi.connect(mContext, mTrackerHandler);
+    }
+
+    @Override
+    public int registerNetworkProvider(Messenger messenger, String name) {
+        enforceNetworkFactoryOrSettingsPermission();
+        Objects.requireNonNull(messenger, "messenger must be non-null");
+        NetworkProviderInfo npi = new NetworkProviderInfo(name, messenger,
+                nextNetworkProviderId(), () -> unregisterNetworkProvider(messenger));
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_PROVIDER, npi));
+        return npi.providerId;
+    }
+
+    @Override
+    public void unregisterNetworkProvider(Messenger messenger) {
+        enforceNetworkFactoryOrSettingsPermission();
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_UNREGISTER_NETWORK_PROVIDER, messenger));
+    }
+
+    @Override
+    public void offerNetwork(final int providerId,
+            @NonNull final NetworkScore score, @NonNull final NetworkCapabilities caps,
+            @NonNull final INetworkOfferCallback callback) {
+        Objects.requireNonNull(score);
+        Objects.requireNonNull(caps);
+        Objects.requireNonNull(callback);
+        final NetworkOffer offer = new NetworkOffer(
+                FullScore.makeProspectiveScore(score, caps), caps, callback, providerId);
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_OFFER, offer));
+    }
+
+    @Override
+    public void unofferNetwork(@NonNull final INetworkOfferCallback callback) {
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_UNREGISTER_NETWORK_OFFER, callback));
+    }
+
+    private void handleUnregisterNetworkProvider(Messenger messenger) {
+        NetworkProviderInfo npi = mNetworkProviderInfos.remove(messenger);
+        if (npi == null) {
+            loge("Failed to find Messenger in unregisterNetworkProvider");
+            return;
+        }
+        // Unregister all the offers from this provider
+        final ArrayList<NetworkOfferInfo> toRemove = new ArrayList<>();
+        for (final NetworkOfferInfo noi : mNetworkOffers) {
+            if (noi.offer.providerId == npi.providerId) {
+                // Can't call handleUnregisterNetworkOffer here because iteration is in progress
+                toRemove.add(noi);
+            }
+        }
+        for (final NetworkOfferInfo noi : toRemove) {
+            handleUnregisterNetworkOffer(noi);
+        }
+        if (DBG) log("unregisterNetworkProvider for " + npi.name);
+    }
+
+    @Override
+    public void declareNetworkRequestUnfulfillable(@NonNull final NetworkRequest request) {
+        if (request.hasTransport(TRANSPORT_TEST)) {
+            enforceNetworkFactoryOrTestNetworksPermission();
+        } else {
+            enforceNetworkFactoryPermission();
+        }
+        final NetworkRequestInfo nri = mNetworkRequests.get(request);
+        if (nri != null) {
+            // declareNetworkRequestUnfulfillable() paths don't apply to multilayer requests.
+            ensureNotMultilayerRequest(nri, "declareNetworkRequestUnfulfillable");
+            mHandler.post(() -> handleReleaseNetworkRequest(
+                    nri.mRequests.get(0), mDeps.getCallingUid(), true));
+        }
+    }
+
+    // NOTE: Accessed on multiple threads, must be synchronized on itself.
+    @GuardedBy("mNetworkForNetId")
+    private final SparseArray<NetworkAgentInfo> mNetworkForNetId = new SparseArray<>();
+    // NOTE: Accessed on multiple threads, synchronized with mNetworkForNetId.
+    // An entry is first reserved with NetIdManager, prior to being added to mNetworkForNetId, so
+    // there may not be a strict 1:1 correlation between the two.
+    private final NetIdManager mNetIdManager;
+
+    // Tracks all NetworkAgents that are currently registered.
+    // NOTE: Only should be accessed on ConnectivityServiceThread, except dump().
+    private final ArraySet<NetworkAgentInfo> mNetworkAgentInfos = new ArraySet<>();
+
+    // UID ranges for users that are currently blocked by VPNs.
+    // This array is accessed and iterated on multiple threads without holding locks, so its
+    // contents must never be mutated. When the ranges change, the array is replaced with a new one
+    // (on the handler thread).
+    private volatile List<UidRange> mVpnBlockedUidRanges = new ArrayList<>();
+
+    // Must only be accessed on the handler thread
+    @NonNull
+    private final ArrayList<NetworkOfferInfo> mNetworkOffers = new ArrayList<>();
+
+    @GuardedBy("mBlockedAppUids")
+    private final HashSet<Integer> mBlockedAppUids = new HashSet<>();
+
+    // Current OEM network preferences. This object must only be written to on the handler thread.
+    // Since it is immutable and always non-null, other threads may read it if they only care
+    // about seeing a consistent object but not that it is current.
+    @NonNull
+    private OemNetworkPreferences mOemNetworkPreferences =
+            new OemNetworkPreferences.Builder().build();
+    // Current per-profile network preferences. This object follows the same threading rules as
+    // the OEM network preferences above.
+    @NonNull
+    private ProfileNetworkPreferences mProfileNetworkPreferences = new ProfileNetworkPreferences();
+
+    // A set of UIDs that should use mobile data preferentially if available. This object follows
+    // the same threading rules as the OEM network preferences above.
+    @NonNull
+    private Set<Integer> mMobileDataPreferredUids = new ArraySet<>();
+
+    // OemNetworkPreferences activity String log entries.
+    private static final int MAX_OEM_NETWORK_PREFERENCE_LOGS = 20;
+    @NonNull
+    private final LocalLog mOemNetworkPreferencesLogs =
+            new LocalLog(MAX_OEM_NETWORK_PREFERENCE_LOGS);
+
+    /**
+     * Determine whether a given package has a mapping in the current OemNetworkPreferences.
+     * @param packageName the package name to check existence of a mapping for.
+     * @return true if a mapping exists, false otherwise
+     */
+    private boolean isMappedInOemNetworkPreference(@NonNull final String packageName) {
+        return mOemNetworkPreferences.getNetworkPreferences().containsKey(packageName);
+    }
+
+    // The always-on request for an Internet-capable network that apps without a specific default
+    // fall back to.
+    @VisibleForTesting
+    @NonNull
+    final NetworkRequestInfo mDefaultRequest;
+    // Collection of NetworkRequestInfo's used for default networks.
+    @VisibleForTesting
+    @NonNull
+    final ArraySet<NetworkRequestInfo> mDefaultNetworkRequests = new ArraySet<>();
+
+    private boolean isPerAppDefaultRequest(@NonNull final NetworkRequestInfo nri) {
+        return (mDefaultNetworkRequests.contains(nri) && mDefaultRequest != nri);
+    }
+
+    /**
+     * Return the default network request currently tracking the given uid.
+     * @param uid the uid to check.
+     * @return the NetworkRequestInfo tracking the given uid.
+     */
+    @NonNull
+    private NetworkRequestInfo getDefaultRequestTrackingUid(final int uid) {
+        for (final NetworkRequestInfo nri : mDefaultNetworkRequests) {
+            if (nri == mDefaultRequest) {
+                continue;
+            }
+            // Checking the first request is sufficient as only multilayer requests will have more
+            // than one request and for multilayer, all requests will track the same uids.
+            if (nri.mRequests.get(0).networkCapabilities.appliesToUid(uid)) {
+                return nri;
+            }
+        }
+        return mDefaultRequest;
+    }
+
+    /**
+     * Get a copy of the network requests of the default request that is currently tracking the
+     * given uid.
+     * @param asUid the uid on behalf of which to file the request. Different from requestorUid
+     *              when a privileged caller is tracking the default network for another uid.
+     * @param requestorUid the uid to check the default for.
+     * @param requestorPackageName the requestor's package name.
+     * @return a copy of the default's NetworkRequest that is tracking the given uid.
+     */
+    @NonNull
+    private List<NetworkRequest> copyDefaultNetworkRequestsForUid(
+            final int asUid, final int requestorUid, @NonNull final String requestorPackageName) {
+        return copyNetworkRequestsForUid(
+                getDefaultRequestTrackingUid(asUid).mRequests,
+                asUid, requestorUid, requestorPackageName);
+    }
+
+    /**
+     * Copy the given nri's NetworkRequest collection.
+     * @param requestsToCopy the NetworkRequest collection to be copied.
+     * @param asUid the uid on behalf of which to file the request. Different from requestorUid
+     *              when a privileged caller is tracking the default network for another uid.
+     * @param requestorUid the uid to set on the copied collection.
+     * @param requestorPackageName the package name to set on the copied collection.
+     * @return the copied NetworkRequest collection.
+     */
+    @NonNull
+    private List<NetworkRequest> copyNetworkRequestsForUid(
+            @NonNull final List<NetworkRequest> requestsToCopy, final int asUid,
+            final int requestorUid, @NonNull final String requestorPackageName) {
+        final List<NetworkRequest> requests = new ArrayList<>();
+        for (final NetworkRequest nr : requestsToCopy) {
+            requests.add(new NetworkRequest(copyDefaultNetworkCapabilitiesForUid(
+                            nr.networkCapabilities, asUid, requestorUid, requestorPackageName),
+                    nr.legacyType, nextNetworkRequestId(), nr.type));
+        }
+        return requests;
+    }
+
+    @NonNull
+    private NetworkCapabilities copyDefaultNetworkCapabilitiesForUid(
+            @NonNull final NetworkCapabilities netCapToCopy, final int asUid,
+            final int requestorUid, @NonNull final String requestorPackageName) {
+        // These capabilities are for a TRACK_DEFAULT callback, so:
+        // 1. Remove NET_CAPABILITY_VPN, because it's (currently!) the only difference between
+        //    mDefaultRequest and a per-UID default request.
+        //    TODO: stop depending on the fact that these two unrelated things happen to be the same
+        // 2. Always set the UIDs to asUid. restrictRequestUidsForCallerAndSetRequestorInfo will
+        //    not do this in the case of a privileged application.
+        final NetworkCapabilities netCap = new NetworkCapabilities(netCapToCopy);
+        netCap.removeCapability(NET_CAPABILITY_NOT_VPN);
+        netCap.setSingleUid(asUid);
+        restrictRequestUidsForCallerAndSetRequestorInfo(
+                netCap, requestorUid, requestorPackageName);
+        return netCap;
+    }
+
+    /**
+     * Get the nri that is currently being tracked for callbacks by per-app defaults.
+     * @param nr the network request to check for equality against.
+     * @return the nri if one exists, null otherwise.
+     */
+    @Nullable
+    private NetworkRequestInfo getNriForAppRequest(@NonNull final NetworkRequest nr) {
+        for (final NetworkRequestInfo nri : mNetworkRequests.values()) {
+            if (nri.getNetworkRequestForCallback().equals(nr)) {
+                return nri;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Check if an nri is currently being managed by per-app default networking.
+     * @param nri the nri to check.
+     * @return true if this nri is currently being managed by per-app default networking.
+     */
+    private boolean isPerAppTrackedNri(@NonNull final NetworkRequestInfo nri) {
+        // nri.mRequests.get(0) is only different from the original request filed in
+        // nri.getNetworkRequestForCallback() if nri.mRequests was changed by per-app default
+        // functionality therefore if these two don't match, it means this particular nri is
+        // currently being managed by a per-app default.
+        return nri.getNetworkRequestForCallback() != nri.mRequests.get(0);
+    }
+
+    /**
+     * Determine if an nri is a managed default request that disallows default networking.
+     * @param nri the request to evaluate
+     * @return true if device-default networking is disallowed
+     */
+    private boolean isDefaultBlocked(@NonNull final NetworkRequestInfo nri) {
+        // Check if this nri is a managed default that supports the default network at its
+        // lowest priority request.
+        final NetworkRequest defaultNetworkRequest = mDefaultRequest.mRequests.get(0);
+        final NetworkCapabilities lowestPriorityNetCap =
+                nri.mRequests.get(nri.mRequests.size() - 1).networkCapabilities;
+        return isPerAppDefaultRequest(nri)
+                && !(defaultNetworkRequest.networkCapabilities.equalRequestableCapabilities(
+                        lowestPriorityNetCap));
+    }
+
+    // Request used to optionally keep mobile data active even when higher
+    // priority networks like Wi-Fi are active.
+    private final NetworkRequest mDefaultMobileDataRequest;
+
+    // Request used to optionally keep wifi data active even when higher
+    // priority networks like ethernet are active.
+    private final NetworkRequest mDefaultWifiRequest;
+
+    // Request used to optionally keep vehicle internal network always active
+    private final NetworkRequest mDefaultVehicleRequest;
+
+    // Sentinel NAI used to direct apps with default networks that should have no connectivity to a
+    // network with no service. This NAI should never be matched against, nor should any public API
+    // ever return the associated network. For this reason, this NAI is not in the list of available
+    // NAIs. It is used in computeNetworkReassignment() to be set as the satisfier for non-device
+    // default requests that don't support using the device default network which will ultimately
+    // allow ConnectivityService to use this no-service network when calling makeDefaultForApps().
+    @VisibleForTesting
+    final NetworkAgentInfo mNoServiceNetwork;
+
+    // The NetworkAgentInfo currently satisfying the default request, if any.
+    private NetworkAgentInfo getDefaultNetwork() {
+        return mDefaultRequest.mSatisfier;
+    }
+
+    private NetworkAgentInfo getDefaultNetworkForUid(final int uid) {
+        for (final NetworkRequestInfo nri : mDefaultNetworkRequests) {
+            // Currently, all network requests will have the same uids therefore checking the first
+            // one is sufficient. If/when uids are tracked at the nri level, this can change.
+            final Set<UidRange> uids = nri.mRequests.get(0).networkCapabilities.getUidRanges();
+            if (null == uids) {
+                continue;
+            }
+            for (final UidRange range : uids) {
+                if (range.contains(uid)) {
+                    return nri.getSatisfier();
+                }
+            }
+        }
+        return getDefaultNetwork();
+    }
+
+    @Nullable
+    private Network getNetwork(@Nullable NetworkAgentInfo nai) {
+        return nai != null ? nai.network : null;
+    }
+
+    private void ensureRunningOnConnectivityServiceThread() {
+        if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on ConnectivityService thread: "
+                            + Thread.currentThread().getName());
+        }
+    }
+
+    @VisibleForTesting
+    protected boolean isDefaultNetwork(NetworkAgentInfo nai) {
+        return nai == getDefaultNetwork();
+    }
+
+    /**
+     * Register a new agent with ConnectivityService to handle a network.
+     *
+     * @param na a reference for ConnectivityService to contact the agent asynchronously.
+     * @param networkInfo the initial info associated with this network. It can be updated later :
+     *         see {@link #updateNetworkInfo}.
+     * @param linkProperties the initial link properties of this network. They can be updated
+     *         later : see {@link #updateLinkProperties}.
+     * @param networkCapabilities the initial capabilites of this network. They can be updated
+     *         later : see {@link #updateCapabilities}.
+     * @param initialScore the initial score of the network. See
+     *         {@link NetworkAgentInfo#getCurrentScore}.
+     * @param networkAgentConfig metadata about the network. This is never updated.
+     * @param providerId the ID of the provider owning this NetworkAgent.
+     * @return the network created for this agent.
+     */
+    public Network registerNetworkAgent(INetworkAgent na, NetworkInfo networkInfo,
+            LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
+            @NonNull NetworkScore initialScore, NetworkAgentConfig networkAgentConfig,
+            int providerId) {
+        Objects.requireNonNull(networkInfo, "networkInfo must not be null");
+        Objects.requireNonNull(linkProperties, "linkProperties must not be null");
+        Objects.requireNonNull(networkCapabilities, "networkCapabilities must not be null");
+        Objects.requireNonNull(initialScore, "initialScore must not be null");
+        Objects.requireNonNull(networkAgentConfig, "networkAgentConfig must not be null");
+        if (networkCapabilities.hasTransport(TRANSPORT_TEST)) {
+            enforceAnyPermissionOf(Manifest.permission.MANAGE_TEST_NETWORKS);
+        } else {
+            enforceNetworkFactoryPermission();
+        }
+
+        final int uid = mDeps.getCallingUid();
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return registerNetworkAgentInternal(na, networkInfo, linkProperties,
+                    networkCapabilities, initialScore, networkAgentConfig, providerId, uid);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
+            LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
+            NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId,
+            int uid) {
+        if (networkCapabilities.hasTransport(TRANSPORT_TEST)) {
+            // Strictly, sanitizing here is unnecessary as the capabilities will be sanitized in
+            // the call to mixInCapabilities below anyway, but sanitizing here means the NAI never
+            // sees capabilities that may be malicious, which might prevent mistakes in the future.
+            networkCapabilities = new NetworkCapabilities(networkCapabilities);
+            networkCapabilities.restrictCapabilitesForTestNetwork(uid);
+        }
+
+        LinkProperties lp = new LinkProperties(linkProperties);
+
+        final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
+        final NetworkAgentInfo nai = new NetworkAgentInfo(na,
+                new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo), lp, nc,
+                currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
+                this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
+                mQosCallbackTracker, mDeps);
+
+        // Make sure the LinkProperties and NetworkCapabilities reflect what the agent info says.
+        processCapabilitiesFromAgent(nai, nc);
+        nai.getAndSetNetworkCapabilities(mixInCapabilities(nai, nc));
+        processLinkPropertiesFromAgent(nai, nai.linkProperties);
+
+        final String extraInfo = networkInfo.getExtraInfo();
+        final String name = TextUtils.isEmpty(extraInfo)
+                ? nai.networkCapabilities.getSsid() : extraInfo;
+        if (DBG) log("registerNetworkAgent " + nai);
+        mDeps.getNetworkStack().makeNetworkMonitor(
+                nai.network, name, new NetworkMonitorCallbacks(nai));
+        // NetworkAgentInfo registration will finish when the NetworkMonitor is created.
+        // If the network disconnects or sends any other event before that, messages are deferred by
+        // NetworkAgent until nai.connect(), which will be called when finalizing the
+        // registration.
+        return nai.network;
+    }
+
+    private void handleRegisterNetworkAgent(NetworkAgentInfo nai, INetworkMonitor networkMonitor) {
+        nai.onNetworkMonitorCreated(networkMonitor);
+        if (VDBG) log("Got NetworkAgent Messenger");
+        mNetworkAgentInfos.add(nai);
+        synchronized (mNetworkForNetId) {
+            mNetworkForNetId.put(nai.network.getNetId(), nai);
+        }
+
+        try {
+            networkMonitor.start();
+        } catch (RemoteException e) {
+            e.rethrowAsRuntimeException();
+        }
+        nai.notifyRegistered();
+        NetworkInfo networkInfo = nai.networkInfo;
+        updateNetworkInfo(nai, networkInfo);
+        updateUids(nai, null, nai.networkCapabilities);
+    }
+
+    private class NetworkOfferInfo implements IBinder.DeathRecipient {
+        @NonNull public final NetworkOffer offer;
+
+        NetworkOfferInfo(@NonNull final NetworkOffer offer) {
+            this.offer = offer;
+        }
+
+        @Override
+        public void binderDied() {
+            mHandler.post(() -> handleUnregisterNetworkOffer(this));
+        }
+    }
+
+    private boolean isNetworkProviderWithIdRegistered(final int providerId) {
+        for (final NetworkProviderInfo npi : mNetworkProviderInfos.values()) {
+            if (npi.providerId == providerId) return true;
+        }
+        return false;
+    }
+
+    /**
+     * Register or update a network offer.
+     * @param newOffer The new offer. If the callback member is the same as an existing
+     *                 offer, it is an update of that offer.
+     */
+    private void handleRegisterNetworkOffer(@NonNull final NetworkOffer newOffer) {
+        ensureRunningOnConnectivityServiceThread();
+        if (!isNetworkProviderWithIdRegistered(newOffer.providerId)) {
+            // This may actually happen if a provider updates its score or registers and then
+            // immediately unregisters. The offer would still be in the handler queue, but the
+            // provider would have been removed.
+            if (DBG) log("Received offer from an unregistered provider");
+            return;
+        }
+
+        final NetworkOfferInfo existingOffer = findNetworkOfferInfoByCallback(newOffer.callback);
+        if (null != existingOffer) {
+            handleUnregisterNetworkOffer(existingOffer);
+            newOffer.migrateFrom(existingOffer.offer);
+        }
+        final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
+        try {
+            noi.offer.callback.asBinder().linkToDeath(noi, 0 /* flags */);
+        } catch (RemoteException e) {
+            noi.binderDied();
+            return;
+        }
+        mNetworkOffers.add(noi);
+        issueNetworkNeeds(noi);
+    }
+
+    private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi) {
+        ensureRunningOnConnectivityServiceThread();
+        mNetworkOffers.remove(noi);
+        noi.offer.callback.asBinder().unlinkToDeath(noi, 0 /* flags */);
+    }
+
+    @Nullable private NetworkOfferInfo findNetworkOfferInfoByCallback(
+            @NonNull final INetworkOfferCallback callback) {
+        ensureRunningOnConnectivityServiceThread();
+        for (final NetworkOfferInfo noi : mNetworkOffers) {
+            if (noi.offer.callback.asBinder().equals(callback.asBinder())) return noi;
+        }
+        return null;
+    }
+
+    /**
+     * Called when receiving LinkProperties directly from a NetworkAgent.
+     * Stores into |nai| any data coming from the agent that might also be written to the network's
+     * LinkProperties by ConnectivityService itself. This ensures that the data provided by the
+     * agent is not lost when updateLinkProperties is called.
+     * This method should never alter the agent's LinkProperties, only store data in |nai|.
+     */
+    private void processLinkPropertiesFromAgent(NetworkAgentInfo nai, LinkProperties lp) {
+        lp.ensureDirectlyConnectedRoutes();
+        nai.clatd.setNat64PrefixFromRa(lp.getNat64Prefix());
+        nai.networkAgentPortalData = lp.getCaptivePortalData();
+    }
+
+    private void updateLinkProperties(NetworkAgentInfo networkAgent, @NonNull LinkProperties newLp,
+            @NonNull LinkProperties oldLp) {
+        int netId = networkAgent.network.getNetId();
+
+        // The NetworkAgent does not know whether clatd is running on its network or not, or whether
+        // a NAT64 prefix was discovered by the DNS resolver. Before we do anything else, make sure
+        // the LinkProperties for the network are accurate.
+        networkAgent.clatd.fixupLinkProperties(oldLp, newLp);
+
+        updateInterfaces(newLp, oldLp, netId, networkAgent.networkCapabilities);
+
+        // update filtering rules, need to happen after the interface update so netd knows about the
+        // new interface (the interface name -> index map becomes initialized)
+        updateVpnFiltering(newLp, oldLp, networkAgent);
+
+        updateMtu(newLp, oldLp);
+        // TODO - figure out what to do for clat
+//        for (LinkProperties lp : newLp.getStackedLinks()) {
+//            updateMtu(lp, null);
+//        }
+        if (isDefaultNetwork(networkAgent)) {
+            updateTcpBufferSizes(newLp.getTcpBufferSizes());
+        }
+
+        updateRoutes(newLp, oldLp, netId);
+        updateDnses(newLp, oldLp, netId);
+        // Make sure LinkProperties represents the latest private DNS status.
+        // This does not need to be done before updateDnses because the
+        // LinkProperties are not the source of the private DNS configuration.
+        // updateDnses will fetch the private DNS configuration from DnsManager.
+        mDnsManager.updatePrivateDnsStatus(netId, newLp);
+
+        if (isDefaultNetwork(networkAgent)) {
+            handleApplyDefaultProxy(newLp.getHttpProxy());
+        } else {
+            updateProxy(newLp, oldLp);
+        }
+
+        updateWakeOnLan(newLp);
+
+        // Captive portal data is obtained from NetworkMonitor and stored in NetworkAgentInfo.
+        // It is not always contained in the LinkProperties sent from NetworkAgents, and if it
+        // does, it needs to be merged here.
+        newLp.setCaptivePortalData(mergeCaptivePortalData(networkAgent.networkAgentPortalData,
+                networkAgent.capportApiData));
+
+        // TODO - move this check to cover the whole function
+        if (!Objects.equals(newLp, oldLp)) {
+            synchronized (networkAgent) {
+                networkAgent.linkProperties = newLp;
+            }
+            // Start or stop DNS64 detection and 464xlat according to network state.
+            networkAgent.clatd.update();
+            notifyIfacesChangedForNetworkStats();
+            networkAgent.networkMonitor().notifyLinkPropertiesChanged(
+                    new LinkProperties(newLp, true /* parcelSensitiveFields */));
+            if (networkAgent.everConnected) {
+                notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_IP_CHANGED);
+            }
+        }
+
+        mKeepaliveTracker.handleCheckKeepalivesStillValid(networkAgent);
+    }
+
+    /**
+     * @param naData captive portal data from NetworkAgent
+     * @param apiData captive portal data from capport API
+     */
+    @Nullable
+    private CaptivePortalData mergeCaptivePortalData(CaptivePortalData naData,
+            CaptivePortalData apiData) {
+        if (naData == null || apiData == null) {
+            return naData == null ? apiData : naData;
+        }
+        final CaptivePortalData.Builder captivePortalBuilder =
+                new CaptivePortalData.Builder(naData);
+
+        if (apiData.isCaptive()) {
+            captivePortalBuilder.setCaptive(true);
+        }
+        if (apiData.isSessionExtendable()) {
+            captivePortalBuilder.setSessionExtendable(true);
+        }
+        if (apiData.getExpiryTimeMillis() >= 0 || apiData.getByteLimit() >= 0) {
+            // Expiry time, bytes remaining, refresh time all need to come from the same source,
+            // otherwise data would be inconsistent. Prefer the capport API info if present,
+            // as it can generally be refreshed more often.
+            captivePortalBuilder.setExpiryTime(apiData.getExpiryTimeMillis());
+            captivePortalBuilder.setBytesRemaining(apiData.getByteLimit());
+            captivePortalBuilder.setRefreshTime(apiData.getRefreshTimeMillis());
+        } else if (naData.getExpiryTimeMillis() < 0 && naData.getByteLimit() < 0) {
+            // No source has time / bytes remaining information: surface the newest refresh time
+            // for other fields
+            captivePortalBuilder.setRefreshTime(
+                    Math.max(naData.getRefreshTimeMillis(), apiData.getRefreshTimeMillis()));
+        }
+
+        // Prioritize the user portal URL from the network agent if the source is authenticated.
+        if (apiData.getUserPortalUrl() != null && naData.getUserPortalUrlSource()
+                != CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) {
+            captivePortalBuilder.setUserPortalUrl(apiData.getUserPortalUrl(),
+                    apiData.getUserPortalUrlSource());
+        }
+        // Prioritize the venue information URL from the network agent if the source is
+        // authenticated.
+        if (apiData.getVenueInfoUrl() != null && naData.getVenueInfoUrlSource()
+                != CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) {
+            captivePortalBuilder.setVenueInfoUrl(apiData.getVenueInfoUrl(),
+                    apiData.getVenueInfoUrlSource());
+        }
+        return captivePortalBuilder.build();
+    }
+
+    private void wakeupModifyInterface(String iface, NetworkCapabilities caps, boolean add) {
+        // Marks are only available on WiFi interfaces. Checking for
+        // marks on unsupported interfaces is harmless.
+        if (!caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+            return;
+        }
+
+        int mark = mResources.get().getInteger(R.integer.config_networkWakeupPacketMark);
+        int mask = mResources.get().getInteger(R.integer.config_networkWakeupPacketMask);
+
+        // TODO (b/183076074): remove legacy fallback after migrating overlays
+        final int legacyMark = mContext.getResources().getInteger(mContext.getResources()
+                .getIdentifier("config_networkWakeupPacketMark", "integer", "android"));
+        final int legacyMask = mContext.getResources().getInteger(mContext.getResources()
+                .getIdentifier("config_networkWakeupPacketMask", "integer", "android"));
+        mark = mark == 0 ? legacyMark : mark;
+        mask = mask == 0 ? legacyMask : mask;
+
+        // Mask/mark of zero will not detect anything interesting.
+        // Don't install rules unless both values are nonzero.
+        if (mark == 0 || mask == 0) {
+            return;
+        }
+
+        final String prefix = "iface:" + iface;
+        try {
+            if (add) {
+                mNetd.wakeupAddInterface(iface, prefix, mark, mask);
+            } else {
+                mNetd.wakeupDelInterface(iface, prefix, mark, mask);
+            }
+        } catch (Exception e) {
+            loge("Exception modifying wakeup packet monitoring: " + e);
+        }
+
+    }
+
+    private void updateInterfaces(final @Nullable LinkProperties newLp,
+            final @Nullable LinkProperties oldLp, final int netId,
+            final @NonNull NetworkCapabilities caps) {
+        final CompareResult<String> interfaceDiff = new CompareResult<>(
+                oldLp != null ? oldLp.getAllInterfaceNames() : null,
+                newLp != null ? newLp.getAllInterfaceNames() : null);
+        if (!interfaceDiff.added.isEmpty()) {
+            for (final String iface : interfaceDiff.added) {
+                try {
+                    if (DBG) log("Adding iface " + iface + " to network " + netId);
+                    mNetd.networkAddInterface(netId, iface);
+                    wakeupModifyInterface(iface, caps, true);
+                    mDeps.reportNetworkInterfaceForTransports(mContext, iface,
+                            caps.getTransportTypes());
+                } catch (Exception e) {
+                    logw("Exception adding interface: " + e);
+                }
+            }
+        }
+        for (final String iface : interfaceDiff.removed) {
+            try {
+                if (DBG) log("Removing iface " + iface + " from network " + netId);
+                wakeupModifyInterface(iface, caps, false);
+                mNetd.networkRemoveInterface(netId, iface);
+            } catch (Exception e) {
+                loge("Exception removing interface: " + e);
+            }
+        }
+    }
+
+    // TODO: move to frameworks/libs/net.
+    private RouteInfoParcel convertRouteInfo(RouteInfo route) {
+        final String nextHop;
+
+        switch (route.getType()) {
+            case RouteInfo.RTN_UNICAST:
+                if (route.hasGateway()) {
+                    nextHop = route.getGateway().getHostAddress();
+                } else {
+                    nextHop = INetd.NEXTHOP_NONE;
+                }
+                break;
+            case RouteInfo.RTN_UNREACHABLE:
+                nextHop = INetd.NEXTHOP_UNREACHABLE;
+                break;
+            case RouteInfo.RTN_THROW:
+                nextHop = INetd.NEXTHOP_THROW;
+                break;
+            default:
+                nextHop = INetd.NEXTHOP_NONE;
+                break;
+        }
+
+        final RouteInfoParcel rip = new RouteInfoParcel();
+        rip.ifName = route.getInterface();
+        rip.destination = route.getDestination().toString();
+        rip.nextHop = nextHop;
+        rip.mtu = route.getMtu();
+
+        return rip;
+    }
+
+    /**
+     * Have netd update routes from oldLp to newLp.
+     * @return true if routes changed between oldLp and newLp
+     */
+    private boolean updateRoutes(LinkProperties newLp, LinkProperties oldLp, int netId) {
+        // compare the route diff to determine which routes have been updated
+        final CompareOrUpdateResult<RouteInfo.RouteKey, RouteInfo> routeDiff =
+                new CompareOrUpdateResult<>(
+                        oldLp != null ? oldLp.getAllRoutes() : null,
+                        newLp != null ? newLp.getAllRoutes() : null,
+                        (r) -> r.getRouteKey());
+
+        // add routes before removing old in case it helps with continuous connectivity
+
+        // do this twice, adding non-next-hop routes first, then routes they are dependent on
+        for (RouteInfo route : routeDiff.added) {
+            if (route.hasGateway()) continue;
+            if (VDBG || DDBG) log("Adding Route [" + route + "] to network " + netId);
+            try {
+                mNetd.networkAddRouteParcel(netId, convertRouteInfo(route));
+            } catch (Exception e) {
+                if ((route.getDestination().getAddress() instanceof Inet4Address) || VDBG) {
+                    loge("Exception in networkAddRouteParcel for non-gateway: " + e);
+                }
+            }
+        }
+        for (RouteInfo route : routeDiff.added) {
+            if (!route.hasGateway()) continue;
+            if (VDBG || DDBG) log("Adding Route [" + route + "] to network " + netId);
+            try {
+                mNetd.networkAddRouteParcel(netId, convertRouteInfo(route));
+            } catch (Exception e) {
+                if ((route.getGateway() instanceof Inet4Address) || VDBG) {
+                    loge("Exception in networkAddRouteParcel for gateway: " + e);
+                }
+            }
+        }
+
+        for (RouteInfo route : routeDiff.removed) {
+            if (VDBG || DDBG) log("Removing Route [" + route + "] from network " + netId);
+            try {
+                mNetd.networkRemoveRouteParcel(netId, convertRouteInfo(route));
+            } catch (Exception e) {
+                loge("Exception in networkRemoveRouteParcel: " + e);
+            }
+        }
+
+        for (RouteInfo route : routeDiff.updated) {
+            if (VDBG || DDBG) log("Updating Route [" + route + "] from network " + netId);
+            try {
+                mNetd.networkUpdateRouteParcel(netId, convertRouteInfo(route));
+            } catch (Exception e) {
+                loge("Exception in networkUpdateRouteParcel: " + e);
+            }
+        }
+        return !routeDiff.added.isEmpty() || !routeDiff.removed.isEmpty()
+                || !routeDiff.updated.isEmpty();
+    }
+
+    private void updateDnses(LinkProperties newLp, LinkProperties oldLp, int netId) {
+        if (oldLp != null && newLp.isIdenticalDnses(oldLp)) {
+            return;  // no updating necessary
+        }
+
+        if (DBG) {
+            final Collection<InetAddress> dnses = newLp.getDnsServers();
+            log("Setting DNS servers for network " + netId + " to " + dnses);
+        }
+        try {
+            mDnsManager.noteDnsServersForNetwork(netId, newLp);
+            mDnsManager.flushVmDnsCache();
+        } catch (Exception e) {
+            loge("Exception in setDnsConfigurationForNetwork: " + e);
+        }
+    }
+
+    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);
+
+        if (!wasFiltering && !needsFiltering) {
+            // Nothing to do.
+            return;
+        }
+
+        if (Objects.equals(oldIface, newIface) && (wasFiltering == needsFiltering)) {
+            // Nothing changed.
+            return;
+        }
+
+        final Set<UidRange> ranges = nai.networkCapabilities.getUidRanges();
+        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.
+        if (wasFiltering) {
+            mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid);
+        }
+        if (needsFiltering) {
+            mPermissionMonitor.onVpnUidRangesAdded(newIface, ranges, vpnAppUid);
+        }
+    }
+
+    private void updateWakeOnLan(@NonNull LinkProperties lp) {
+        if (mWolSupportedInterfaces == null) {
+            mWolSupportedInterfaces = new ArraySet<>(mResources.get().getStringArray(
+                    R.array.config_wakeonlan_supported_interfaces));
+        }
+        lp.setWakeOnLanSupported(mWolSupportedInterfaces.contains(lp.getInterfaceName()));
+    }
+
+    private int getNetworkPermission(NetworkCapabilities nc) {
+        if (!nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+            return INetd.PERMISSION_SYSTEM;
+        }
+        if (!nc.hasCapability(NET_CAPABILITY_FOREGROUND)) {
+            return INetd.PERMISSION_NETWORK;
+        }
+        return INetd.PERMISSION_NONE;
+    }
+
+    private void updateNetworkPermissions(@NonNull final NetworkAgentInfo nai,
+            @NonNull final NetworkCapabilities newNc) {
+        final int oldPermission = getNetworkPermission(nai.networkCapabilities);
+        final int newPermission = getNetworkPermission(newNc);
+        if (oldPermission != newPermission && nai.created && !nai.isVPN()) {
+            try {
+                mNetd.networkSetPermissionForNetwork(nai.network.getNetId(), newPermission);
+            } catch (RemoteException | ServiceSpecificException e) {
+                loge("Exception in networkSetPermissionForNetwork: " + e);
+            }
+        }
+    }
+
+    /**
+     * Called when receiving NetworkCapabilities directly from a NetworkAgent.
+     * Stores into |nai| any data coming from the agent that might also be written to the network's
+     * NetworkCapabilities by ConnectivityService itself. This ensures that the data provided by the
+     * agent is not lost when updateCapabilities is called.
+     * This method should never alter the agent's NetworkCapabilities, only store data in |nai|.
+     */
+    private void processCapabilitiesFromAgent(NetworkAgentInfo nai, NetworkCapabilities nc) {
+        // Note: resetting the owner UID before storing the agent capabilities in NAI means that if
+        // the agent attempts to change the owner UID, then nai.declaredCapabilities will not
+        // actually be the same as the capabilities sent by the agent. Still, it is safer to reset
+        // the owner UID here and behave as if the agent had never tried to change it.
+        if (nai.networkCapabilities.getOwnerUid() != nc.getOwnerUid()) {
+            Log.e(TAG, nai.toShortString() + ": ignoring attempt to change owner from "
+                    + nai.networkCapabilities.getOwnerUid() + " to " + nc.getOwnerUid());
+            nc.setOwnerUid(nai.networkCapabilities.getOwnerUid());
+        }
+        nai.declaredCapabilities = new NetworkCapabilities(nc);
+    }
+
+    /** Modifies |newNc| based on the capabilities of |underlyingNetworks| and |agentCaps|. */
+    @VisibleForTesting
+    void applyUnderlyingCapabilities(@Nullable Network[] underlyingNetworks,
+            @NonNull NetworkCapabilities agentCaps, @NonNull NetworkCapabilities newNc) {
+        underlyingNetworks = underlyingNetworksOrDefault(
+                agentCaps.getOwnerUid(), underlyingNetworks);
+        long transportTypes = NetworkCapabilitiesUtils.packBits(agentCaps.getTransportTypes());
+        int downKbps = NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED;
+        int upKbps = NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED;
+        // metered if any underlying is metered, or originally declared metered by the agent.
+        boolean metered = !agentCaps.hasCapability(NET_CAPABILITY_NOT_METERED);
+        boolean roaming = false; // roaming if any underlying is roaming
+        boolean congested = false; // congested if any underlying is congested
+        boolean suspended = true; // suspended if all underlying are suspended
+
+        boolean hadUnderlyingNetworks = false;
+        if (null != underlyingNetworks) {
+            for (Network underlyingNetwork : underlyingNetworks) {
+                final NetworkAgentInfo underlying =
+                        getNetworkAgentInfoForNetwork(underlyingNetwork);
+                if (underlying == null) continue;
+
+                final NetworkCapabilities underlyingCaps = underlying.networkCapabilities;
+                hadUnderlyingNetworks = true;
+                for (int underlyingType : underlyingCaps.getTransportTypes()) {
+                    transportTypes |= 1L << underlyingType;
+                }
+
+                // Merge capabilities of this underlying network. For bandwidth, assume the
+                // worst case.
+                downKbps = NetworkCapabilities.minBandwidth(downKbps,
+                        underlyingCaps.getLinkDownstreamBandwidthKbps());
+                upKbps = NetworkCapabilities.minBandwidth(upKbps,
+                        underlyingCaps.getLinkUpstreamBandwidthKbps());
+                // If this underlying network is metered, the VPN is metered (it may cost money
+                // to send packets on this network).
+                metered |= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_METERED);
+                // If this underlying network is roaming, the VPN is roaming (the billing structure
+                // is different than the usual, local one).
+                roaming |= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_ROAMING);
+                // If this underlying network is congested, the VPN is congested (the current
+                // condition of the network affects the performance of this network).
+                congested |= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_CONGESTED);
+                // If this network is not suspended, the VPN is not suspended (the VPN
+                // is able to transfer some data).
+                suspended &= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+            }
+        }
+        if (!hadUnderlyingNetworks) {
+            // No idea what the underlying networks are; assume reasonable defaults
+            metered = true;
+            roaming = false;
+            congested = false;
+            suspended = false;
+        }
+
+        newNc.setTransportTypes(NetworkCapabilitiesUtils.unpackBits(transportTypes));
+        newNc.setLinkDownstreamBandwidthKbps(downKbps);
+        newNc.setLinkUpstreamBandwidthKbps(upKbps);
+        newNc.setCapability(NET_CAPABILITY_NOT_METERED, !metered);
+        newNc.setCapability(NET_CAPABILITY_NOT_ROAMING, !roaming);
+        newNc.setCapability(NET_CAPABILITY_NOT_CONGESTED, !congested);
+        newNc.setCapability(NET_CAPABILITY_NOT_SUSPENDED, !suspended);
+    }
+
+    /**
+     * Augments the NetworkCapabilities passed in by a NetworkAgent with capabilities that are
+     * maintained here that the NetworkAgent is not aware of (e.g., validated, captive portal,
+     * and foreground status).
+     */
+    @NonNull
+    private NetworkCapabilities mixInCapabilities(NetworkAgentInfo nai, NetworkCapabilities nc) {
+        // Once a NetworkAgent is connected, complain if some immutable capabilities are removed.
+         // Don't complain for VPNs since they're not driven by requests and there is no risk of
+         // causing a connect/teardown loop.
+         // TODO: remove this altogether and make it the responsibility of the NetworkProviders to
+         // avoid connect/teardown loops.
+        if (nai.everConnected &&
+                !nai.isVPN() &&
+                !nai.networkCapabilities.satisfiedByImmutableNetworkCapabilities(nc)) {
+            // TODO: consider not complaining when a network agent degrades its capabilities if this
+            // does not cause any request (that is not a listen) currently matching that agent to
+            // stop being matched by the updated agent.
+            String diff = nai.networkCapabilities.describeImmutableDifferences(nc);
+            if (!TextUtils.isEmpty(diff)) {
+                Log.wtf(TAG, "BUG: " + nai + " lost immutable capabilities:" + diff);
+            }
+        }
+
+        // Don't modify caller's NetworkCapabilities.
+        final NetworkCapabilities newNc = new NetworkCapabilities(nc);
+        if (nai.lastValidated) {
+            newNc.addCapability(NET_CAPABILITY_VALIDATED);
+        } else {
+            newNc.removeCapability(NET_CAPABILITY_VALIDATED);
+        }
+        if (nai.lastCaptivePortalDetected) {
+            newNc.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
+        } else {
+            newNc.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
+        }
+        if (nai.isBackgroundNetwork()) {
+            newNc.removeCapability(NET_CAPABILITY_FOREGROUND);
+        } else {
+            newNc.addCapability(NET_CAPABILITY_FOREGROUND);
+        }
+        if (nai.partialConnectivity) {
+            newNc.addCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY);
+        } else {
+            newNc.removeCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY);
+        }
+        newNc.setPrivateDnsBroken(nai.networkCapabilities.isPrivateDnsBroken());
+
+        // TODO : remove this once all factories are updated to send NOT_SUSPENDED and NOT_ROAMING
+        if (!newNc.hasTransport(TRANSPORT_CELLULAR)) {
+            newNc.addCapability(NET_CAPABILITY_NOT_SUSPENDED);
+            newNc.addCapability(NET_CAPABILITY_NOT_ROAMING);
+        }
+
+        if (nai.supportsUnderlyingNetworks()) {
+            applyUnderlyingCapabilities(nai.declaredUnderlyingNetworks, nai.declaredCapabilities,
+                    newNc);
+        }
+
+        return newNc;
+    }
+
+    private void updateNetworkInfoForRoamingAndSuspended(NetworkAgentInfo nai,
+            NetworkCapabilities prevNc, NetworkCapabilities newNc) {
+        final boolean prevSuspended = !prevNc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        final boolean suspended = !newNc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        final boolean prevRoaming = !prevNc.hasCapability(NET_CAPABILITY_NOT_ROAMING);
+        final boolean roaming = !newNc.hasCapability(NET_CAPABILITY_NOT_ROAMING);
+        if (prevSuspended != suspended) {
+            // TODO (b/73132094) : remove this call once the few users of onSuspended and
+            // onResumed have been removed.
+            notifyNetworkCallbacks(nai, suspended ? ConnectivityManager.CALLBACK_SUSPENDED
+                    : ConnectivityManager.CALLBACK_RESUMED);
+        }
+        if (prevSuspended != suspended || prevRoaming != roaming) {
+            // updateNetworkInfo will mix in the suspended info from the capabilities and
+            // take appropriate action for the network having possibly changed state.
+            updateNetworkInfo(nai, nai.networkInfo);
+        }
+    }
+
+    /**
+     * Update the NetworkCapabilities for {@code nai} to {@code nc}. Specifically:
+     *
+     * 1. Calls mixInCapabilities to merge the passed-in NetworkCapabilities {@code nc} with the
+     *    capabilities we manage and store in {@code nai}, such as validated status and captive
+     *    portal status)
+     * 2. Takes action on the result: changes network permissions, sends CAP_CHANGED callbacks, and
+     *    potentially triggers rematches.
+     * 3. Directly informs other network stack components (NetworkStatsService, VPNs, etc. of the
+     *    change.)
+     *
+     * @param oldScore score of the network before any of the changes that prompted us
+     *                 to call this function.
+     * @param nai the network having its capabilities updated.
+     * @param nc the new network capabilities.
+     */
+    private void updateCapabilities(final int oldScore, @NonNull final NetworkAgentInfo nai,
+            @NonNull final NetworkCapabilities nc) {
+        NetworkCapabilities newNc = mixInCapabilities(nai, nc);
+        if (Objects.equals(nai.networkCapabilities, newNc)) return;
+        updateNetworkPermissions(nai, newNc);
+        final NetworkCapabilities prevNc = nai.getAndSetNetworkCapabilities(newNc);
+
+        updateUids(nai, prevNc, newNc);
+        nai.updateScoreForNetworkAgentUpdate();
+
+        if (nai.getCurrentScore() == oldScore && newNc.equalRequestableCapabilities(prevNc)) {
+            // If the requestable capabilities haven't changed, and the score hasn't changed, then
+            // the change we're processing can't affect any requests, it can only affect the listens
+            // on this network. We might have been called by rematchNetworkAndRequests when a
+            // network changed foreground state.
+            processListenRequests(nai);
+        } else {
+            // If the requestable capabilities have changed or the score changed, we can't have been
+            // called by rematchNetworkAndRequests, so it's safe to start a rematch.
+            rematchAllNetworksAndRequests();
+            notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+        }
+        updateNetworkInfoForRoamingAndSuspended(nai, prevNc, newNc);
+
+        final boolean oldMetered = prevNc.isMetered();
+        final boolean newMetered = newNc.isMetered();
+        final boolean meteredChanged = oldMetered != newMetered;
+
+        if (meteredChanged) {
+            maybeNotifyNetworkBlocked(nai, oldMetered, newMetered,
+                    mVpnBlockedUidRanges, mVpnBlockedUidRanges);
+        }
+
+        final boolean roamingChanged = prevNc.hasCapability(NET_CAPABILITY_NOT_ROAMING)
+                != newNc.hasCapability(NET_CAPABILITY_NOT_ROAMING);
+
+        // Report changes that are interesting for network statistics tracking.
+        if (meteredChanged || roamingChanged) {
+            notifyIfacesChangedForNetworkStats();
+        }
+
+        // This network might have been underlying another network. Propagate its capabilities.
+        propagateUnderlyingNetworkCapabilities(nai.network);
+
+        if (!newNc.equalsTransportTypes(prevNc)) {
+            mDnsManager.updateTransportsForNetwork(
+                    nai.network.getNetId(), newNc.getTransportTypes());
+        }
+
+        maybeSendProxyBroadcast(nai, prevNc, newNc);
+    }
+
+    /** Convenience method to update the capabilities for a given network. */
+    private void updateCapabilitiesForNetwork(NetworkAgentInfo nai) {
+        updateCapabilities(nai.getCurrentScore(), nai, nai.networkCapabilities);
+    }
+
+    /**
+     * Returns whether VPN isolation (ingress interface filtering) should be applied on the given
+     * network.
+     *
+     * 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
+     *  1. the network is an app VPN (not legacy VPN)
+     *  2. the VPN does not allow bypass
+     *  3. the VPN is fully-routed
+     *  4. the VPN interface is non-null
+     *
+     * @see INetd#firewallAddUidInterfaceRules
+     * @see INetd#firewallRemoveUidInterfaceRules
+     */
+    private boolean requiresVpnIsolation(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
+            LinkProperties lp) {
+        if (nc == null || lp == null) return false;
+        return nai.isVPN()
+                && !nai.networkAgentConfig.allowBypass
+                && nc.getOwnerUid() != Process.SYSTEM_UID
+                && lp.getInterfaceName() != null
+                && (lp.hasIpv4DefaultRoute() || lp.hasIpv4UnreachableDefaultRoute())
+                && (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute());
+    }
+
+    private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
+        final UidRangeParcel[] stableRanges = new UidRangeParcel[ranges.size()];
+        int index = 0;
+        for (UidRange range : ranges) {
+            stableRanges[index] = new UidRangeParcel(range.start, range.stop);
+            index++;
+        }
+        return stableRanges;
+    }
+
+    private static UidRangeParcel[] toUidRangeStableParcels(UidRange[] ranges) {
+        final UidRangeParcel[] stableRanges = new UidRangeParcel[ranges.length];
+        for (int i = 0; i < ranges.length; i++) {
+            stableRanges[i] = new UidRangeParcel(ranges[i].start, ranges[i].stop);
+        }
+        return stableRanges;
+    }
+
+    private void maybeCloseSockets(NetworkAgentInfo nai, UidRangeParcel[] ranges,
+            int[] exemptUids) {
+        if (nai.isVPN() && !nai.networkAgentConfig.allowBypass) {
+            try {
+                mNetd.socketDestroy(ranges, exemptUids);
+            } catch (Exception e) {
+                loge("Exception in socket destroy: ", e);
+            }
+        }
+    }
+
+    private void updateUidRanges(boolean add, NetworkAgentInfo nai, Set<UidRange> uidRanges) {
+        int[] exemptUids = new int[2];
+        // TODO: Excluding VPN_UID is necessary in order to not to kill the TCP connection used
+        // by PPTP. Fix this by making Vpn set the owner UID to VPN_UID instead of system when
+        // starting a legacy VPN, and remove VPN_UID here. (b/176542831)
+        exemptUids[0] = VPN_UID;
+        exemptUids[1] = nai.networkCapabilities.getOwnerUid();
+        UidRangeParcel[] ranges = toUidRangeStableParcels(uidRanges);
+
+        maybeCloseSockets(nai, ranges, exemptUids);
+        try {
+            if (add) {
+                // TODO: Passing default network priority to netd.
+                mNetd.networkAddUidRanges(nai.network.netId, ranges
+                        /* DEFAULT_NETWORK_PRIORITY_NONE */);
+            } else {
+                // TODO: Passing default network priority to netd.
+                mNetd.networkRemoveUidRanges(nai.network.netId, ranges
+                        /* DEFAULT_NETWORK_PRIORITY_NONE */);
+            }
+        } catch (Exception e) {
+            loge("Exception while " + (add ? "adding" : "removing") + " uid ranges " + uidRanges +
+                    " on netId " + nai.network.netId + ". " + e);
+        }
+        maybeCloseSockets(nai, ranges, exemptUids);
+    }
+
+    private boolean isProxySetOnAnyDefaultNetwork() {
+        ensureRunningOnConnectivityServiceThread();
+        for (final NetworkRequestInfo nri : mDefaultNetworkRequests) {
+            final NetworkAgentInfo nai = nri.getSatisfier();
+            if (nai != null && nai.linkProperties.getHttpProxy() != null) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void maybeSendProxyBroadcast(NetworkAgentInfo nai, NetworkCapabilities prevNc,
+            NetworkCapabilities newNc) {
+        // When the apps moved from/to a VPN, a proxy broadcast is needed to inform the apps that
+        // the proxy might be changed since the default network satisfied by the apps might also
+        // changed.
+        // TODO: Try to track the default network that apps use and only send a proxy broadcast when
+        //  that happens to prevent false alarms.
+        if (nai.isVPN() && nai.everConnected && !NetworkCapabilities.hasSameUids(prevNc, newNc)
+                && (nai.linkProperties.getHttpProxy() != null || isProxySetOnAnyDefaultNetwork())) {
+            mProxyTracker.sendProxyBroadcast();
+        }
+    }
+
+    private void updateUids(NetworkAgentInfo nai, NetworkCapabilities prevNc,
+            NetworkCapabilities newNc) {
+        Set<UidRange> prevRanges = null == prevNc ? null : prevNc.getUidRanges();
+        Set<UidRange> newRanges = null == newNc ? null : newNc.getUidRanges();
+        if (null == prevRanges) prevRanges = new ArraySet<>();
+        if (null == newRanges) newRanges = new ArraySet<>();
+        final Set<UidRange> prevRangesCopy = new ArraySet<>(prevRanges);
+
+        prevRanges.removeAll(newRanges);
+        newRanges.removeAll(prevRangesCopy);
+
+        try {
+            // When updating the VPN uid routing rules, add the new range first then remove the old
+            // range. If old range were removed first, there would be a window between the old
+            // range being removed and the new range being added, during which UIDs contained
+            // in both ranges are not subject to any VPN routing rules. Adding new range before
+            // removing old range works because, unlike the filtering rules below, it's possible to
+            // add duplicate UID routing rules.
+            // TODO: calculate the intersection of add & remove. Imagining that we are trying to
+            // remove uid 3 from a set containing 1-5. Intersection of the prev and new sets is:
+            //   [1-5] & [1-2],[4-5] == [3]
+            // Then we can do:
+            //   maybeCloseSockets([3])
+            //   mNetd.networkAddUidRanges([1-2],[4-5])
+            //   mNetd.networkRemoveUidRanges([1-5])
+            //   maybeCloseSockets([3])
+            // This can prevent the sockets of uid 1-2, 4-5 from being closed. It also reduce the
+            // number of binder calls from 6 to 4.
+            if (!newRanges.isEmpty()) {
+                updateUidRanges(true, nai, newRanges);
+            }
+            if (!prevRanges.isEmpty()) {
+                updateUidRanges(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();
+            // 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
+            // prevRanges = [0, 99999] while newRanges = [0, 10012], [10014, 99999]. If prevRanges
+            // were added first and then newRanges got removed later, there would be only one uid
+            // 10013 left. A consequence of removing old ranges before adding new ranges is that
+            // there is now a window of opportunity when the UIDs are not subject to any filtering.
+            // Note that this is in contrast with the (more robust) update of VPN routing rules
+            // 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.
+            if (wasFiltering && !prevRanges.isEmpty()) {
+                mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges, prevNc.getOwnerUid());
+            }
+            if (shouldFilter && !newRanges.isEmpty()) {
+                mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges, newNc.getOwnerUid());
+            }
+        } catch (Exception e) {
+            // Never crash!
+            loge("Exception in updateUids: ", e);
+        }
+    }
+
+    public void handleUpdateLinkProperties(NetworkAgentInfo nai, LinkProperties newLp) {
+        ensureRunningOnConnectivityServiceThread();
+
+        if (!mNetworkAgentInfos.contains(nai)) {
+            // Ignore updates for disconnected networks
+            return;
+        }
+        if (VDBG || DDBG) {
+            log("Update of LinkProperties for " + nai.toShortString()
+                    + "; created=" + nai.created
+                    + "; everConnected=" + nai.everConnected);
+        }
+        // TODO: eliminate this defensive copy after confirming that updateLinkProperties does not
+        // modify its oldLp parameter.
+        updateLinkProperties(nai, newLp, new LinkProperties(nai.linkProperties));
+    }
+
+    private void sendPendingIntentForRequest(NetworkRequestInfo nri, NetworkAgentInfo networkAgent,
+            int notificationType) {
+        if (notificationType == ConnectivityManager.CALLBACK_AVAILABLE && !nri.mPendingIntentSent) {
+            Intent intent = new Intent();
+            intent.putExtra(ConnectivityManager.EXTRA_NETWORK, networkAgent.network);
+            // If apps could file multi-layer requests with PendingIntents, they'd need to know
+            // which of the layer is satisfied alongside with some ID for the request. Hence, if
+            // such an API is ever implemented, there is no doubt the right request to send in
+            // EXTRA_NETWORK_REQUEST is mActiveRequest, and whatever ID would be added would need to
+            // be sent as a separate extra.
+            intent.putExtra(ConnectivityManager.EXTRA_NETWORK_REQUEST, nri.getActiveRequest());
+            nri.mPendingIntentSent = true;
+            sendIntent(nri.mPendingIntent, intent);
+        }
+        // else not handled
+    }
+
+    private void sendIntent(PendingIntent pendingIntent, Intent intent) {
+        mPendingIntentWakeLock.acquire();
+        try {
+            if (DBG) log("Sending " + pendingIntent);
+            pendingIntent.send(mContext, 0, intent, this /* onFinished */, null /* Handler */);
+        } catch (PendingIntent.CanceledException e) {
+            if (DBG) log(pendingIntent + " was not sent, it had been canceled.");
+            mPendingIntentWakeLock.release();
+            releasePendingNetworkRequest(pendingIntent);
+        }
+        // ...otherwise, mPendingIntentWakeLock.release() gets called by onSendFinished()
+    }
+
+    @Override
+    public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode,
+            String resultData, Bundle resultExtras) {
+        if (DBG) log("Finished sending " + pendingIntent);
+        mPendingIntentWakeLock.release();
+        // Release with a delay so the receiving client has an opportunity to put in its
+        // own request.
+        releasePendingNetworkRequestWithDelay(pendingIntent);
+    }
+
+    private void callCallbackForRequest(@NonNull final NetworkRequestInfo nri,
+            @NonNull final NetworkAgentInfo networkAgent, final int notificationType,
+            final int arg1) {
+        if (nri.mMessenger == null) {
+            // Default request has no msgr. Also prevents callbacks from being invoked for
+            // NetworkRequestInfos registered with ConnectivityDiagnostics requests. Those callbacks
+            // are Type.LISTEN, but should not have NetworkCallbacks invoked.
+            return;
+        }
+        Bundle bundle = new Bundle();
+        // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
+        // TODO: check if defensive copies of data is needed.
+        final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
+        putParcelable(bundle, nrForCallback);
+        Message msg = Message.obtain();
+        if (notificationType != ConnectivityManager.CALLBACK_UNAVAIL) {
+            putParcelable(bundle, networkAgent.network);
+        }
+        final boolean includeLocationSensitiveInfo =
+                (nri.mCallbackFlags & NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) != 0;
+        switch (notificationType) {
+            case ConnectivityManager.CALLBACK_AVAILABLE: {
+                final NetworkCapabilities nc =
+                        networkCapabilitiesRestrictedForCallerPermissions(
+                                networkAgent.networkCapabilities, nri.mPid, nri.mUid);
+                putParcelable(
+                        bundle,
+                        createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                                nc, includeLocationSensitiveInfo, nri.mPid, nri.mUid,
+                                nrForCallback.getRequestorPackageName(),
+                                nri.mCallingAttributionTag));
+                putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions(
+                        networkAgent.linkProperties, nri.mPid, nri.mUid));
+                // For this notification, arg1 contains the blocked status.
+                msg.arg1 = arg1;
+                break;
+            }
+            case ConnectivityManager.CALLBACK_LOSING: {
+                msg.arg1 = arg1;
+                break;
+            }
+            case ConnectivityManager.CALLBACK_CAP_CHANGED: {
+                // networkAgent can't be null as it has been accessed a few lines above.
+                final NetworkCapabilities netCap =
+                        networkCapabilitiesRestrictedForCallerPermissions(
+                                networkAgent.networkCapabilities, nri.mPid, nri.mUid);
+                putParcelable(
+                        bundle,
+                        createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                                netCap, includeLocationSensitiveInfo, nri.mPid, nri.mUid,
+                                nrForCallback.getRequestorPackageName(),
+                                nri.mCallingAttributionTag));
+                break;
+            }
+            case ConnectivityManager.CALLBACK_IP_CHANGED: {
+                putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions(
+                        networkAgent.linkProperties, nri.mPid, nri.mUid));
+                break;
+            }
+            case ConnectivityManager.CALLBACK_BLK_CHANGED: {
+                maybeLogBlockedStatusChanged(nri, networkAgent.network, arg1);
+                msg.arg1 = arg1;
+                break;
+            }
+        }
+        msg.what = notificationType;
+        msg.setData(bundle);
+        try {
+            if (VDBG) {
+                String notification = ConnectivityManager.getCallbackName(notificationType);
+                log("sending notification " + notification + " for " + nrForCallback);
+            }
+            nri.mMessenger.send(msg);
+        } catch (RemoteException e) {
+            // may occur naturally in the race of binder death.
+            loge("RemoteException caught trying to send a callback msg for " + nrForCallback);
+        }
+    }
+
+    private static <T extends Parcelable> void putParcelable(Bundle bundle, T t) {
+        bundle.putParcelable(t.getClass().getSimpleName(), t);
+    }
+
+    private void teardownUnneededNetwork(NetworkAgentInfo nai) {
+        if (nai.numRequestNetworkRequests() != 0) {
+            for (int i = 0; i < nai.numNetworkRequests(); i++) {
+                NetworkRequest nr = nai.requestAt(i);
+                // Ignore listening and track default requests.
+                if (!nr.isRequest()) continue;
+                loge("Dead network still had at least " + nr);
+                break;
+            }
+        }
+        nai.disconnect();
+    }
+
+    private void handleLingerComplete(NetworkAgentInfo oldNetwork) {
+        if (oldNetwork == null) {
+            loge("Unknown NetworkAgentInfo in handleLingerComplete");
+            return;
+        }
+        if (DBG) log("handleLingerComplete for " + oldNetwork.toShortString());
+
+        // If we get here it means that the last linger timeout for this network expired. So there
+        // must be no other active linger timers, and we must stop lingering.
+        oldNetwork.clearInactivityState();
+
+        if (unneeded(oldNetwork, UnneededFor.TEARDOWN)) {
+            // Tear the network down.
+            teardownUnneededNetwork(oldNetwork);
+        } else {
+            // Put the network in the background if it doesn't satisfy any foreground request.
+            updateCapabilitiesForNetwork(oldNetwork);
+        }
+    }
+
+    private void processDefaultNetworkChanges(@NonNull final NetworkReassignment changes) {
+        boolean isDefaultChanged = false;
+        for (final NetworkRequestInfo defaultRequestInfo : mDefaultNetworkRequests) {
+            final NetworkReassignment.RequestReassignment reassignment =
+                    changes.getReassignment(defaultRequestInfo);
+            if (null == reassignment) {
+                continue;
+            }
+            // reassignment only contains those instances where the satisfying network changed.
+            isDefaultChanged = true;
+            // Notify system services of the new default.
+            makeDefault(defaultRequestInfo, reassignment.mOldNetwork, reassignment.mNewNetwork);
+        }
+
+        if (isDefaultChanged) {
+            // Hold a wakelock for a short time to help apps in migrating to a new default.
+            scheduleReleaseNetworkTransitionWakelock();
+        }
+    }
+
+    private void makeDefault(@NonNull final NetworkRequestInfo nri,
+            @Nullable final NetworkAgentInfo oldDefaultNetwork,
+            @Nullable final NetworkAgentInfo newDefaultNetwork) {
+        if (DBG) {
+            log("Switching to new default network for: " + nri + " using " + newDefaultNetwork);
+        }
+
+        // Fix up the NetworkCapabilities of any networks that have this network as underlying.
+        if (newDefaultNetwork != null) {
+            propagateUnderlyingNetworkCapabilities(newDefaultNetwork.network);
+        }
+
+        // Set an app level managed default and return since further processing only applies to the
+        // default network.
+        if (mDefaultRequest != nri) {
+            makeDefaultForApps(nri, oldDefaultNetwork, newDefaultNetwork);
+            return;
+        }
+
+        makeDefaultNetwork(newDefaultNetwork);
+
+        if (oldDefaultNetwork != null) {
+            mLingerMonitor.noteLingerDefaultNetwork(oldDefaultNetwork, newDefaultNetwork);
+        }
+        mNetworkActivityTracker.updateDataActivityTracking(newDefaultNetwork, oldDefaultNetwork);
+        handleApplyDefaultProxy(null != newDefaultNetwork
+                ? newDefaultNetwork.linkProperties.getHttpProxy() : null);
+        updateTcpBufferSizes(null != newDefaultNetwork
+                ? newDefaultNetwork.linkProperties.getTcpBufferSizes() : null);
+        notifyIfacesChangedForNetworkStats();
+    }
+
+    private void makeDefaultForApps(@NonNull final NetworkRequestInfo nri,
+            @Nullable final NetworkAgentInfo oldDefaultNetwork,
+            @Nullable final NetworkAgentInfo newDefaultNetwork) {
+        try {
+            if (VDBG) {
+                log("Setting default network for " + nri
+                        + " using UIDs " + nri.getUids()
+                        + " with old network " + (oldDefaultNetwork != null
+                        ? oldDefaultNetwork.network().getNetId() : "null")
+                        + " and new network " + (newDefaultNetwork != null
+                        ? newDefaultNetwork.network().getNetId() : "null"));
+            }
+            if (nri.getUids().isEmpty()) {
+                throw new IllegalStateException("makeDefaultForApps called without specifying"
+                        + " any applications to set as the default." + nri);
+            }
+            if (null != newDefaultNetwork) {
+                // TODO: Passing default network priority to netd.
+                mNetd.networkAddUidRanges(
+                        newDefaultNetwork.network.getNetId(),
+                        toUidRangeStableParcels(nri.getUids())
+                        /* nri.getDefaultNetworkPriority() */);
+            }
+            if (null != oldDefaultNetwork) {
+                // TODO: Passing default network priority to netd.
+                mNetd.networkRemoveUidRanges(
+                        oldDefaultNetwork.network.getNetId(),
+                        toUidRangeStableParcels(nri.getUids())
+                        /* nri.getDefaultNetworkPriority() */);
+            }
+        } catch (RemoteException | ServiceSpecificException e) {
+            loge("Exception setting app default network", e);
+        }
+    }
+
+    private void makeDefaultNetwork(@Nullable final NetworkAgentInfo newDefaultNetwork) {
+        try {
+            if (null != newDefaultNetwork) {
+                mNetd.networkSetDefault(newDefaultNetwork.network.getNetId());
+            } else {
+                mNetd.networkClearDefault();
+            }
+        } catch (RemoteException | ServiceSpecificException e) {
+            loge("Exception setting default network :" + e);
+        }
+    }
+
+    private void processListenRequests(@NonNull final NetworkAgentInfo nai) {
+        // For consistency with previous behaviour, send onLost callbacks before onAvailable.
+        processNewlyLostListenRequests(nai);
+        notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+        processNewlySatisfiedListenRequests(nai);
+    }
+
+    private void processNewlyLostListenRequests(@NonNull final NetworkAgentInfo nai) {
+        for (final NetworkRequestInfo nri : mNetworkRequests.values()) {
+            if (nri.isMultilayerRequest()) {
+                continue;
+            }
+            final NetworkRequest nr = nri.mRequests.get(0);
+            if (!nr.isListen()) continue;
+            if (nai.isSatisfyingRequest(nr.requestId) && !nai.satisfies(nr)) {
+                nai.removeRequest(nr.requestId);
+                callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_LOST, 0);
+            }
+        }
+    }
+
+    private void processNewlySatisfiedListenRequests(@NonNull final NetworkAgentInfo nai) {
+        for (final NetworkRequestInfo nri : mNetworkRequests.values()) {
+            if (nri.isMultilayerRequest()) {
+                continue;
+            }
+            final NetworkRequest nr = nri.mRequests.get(0);
+            if (!nr.isListen()) continue;
+            if (nai.satisfies(nr) && !nai.isSatisfyingRequest(nr.requestId)) {
+                nai.addRequest(nr);
+                notifyNetworkAvailable(nai, nri);
+            }
+        }
+    }
+
+    // An accumulator class to gather the list of changes that result from a rematch.
+    private static class NetworkReassignment {
+        static class RequestReassignment {
+            @NonNull public final NetworkRequestInfo mNetworkRequestInfo;
+            @Nullable public final NetworkRequest mOldNetworkRequest;
+            @Nullable public final NetworkRequest mNewNetworkRequest;
+            @Nullable public final NetworkAgentInfo mOldNetwork;
+            @Nullable public final NetworkAgentInfo mNewNetwork;
+            RequestReassignment(@NonNull final NetworkRequestInfo networkRequestInfo,
+                    @Nullable final NetworkRequest oldNetworkRequest,
+                    @Nullable final NetworkRequest newNetworkRequest,
+                    @Nullable final NetworkAgentInfo oldNetwork,
+                    @Nullable final NetworkAgentInfo newNetwork) {
+                mNetworkRequestInfo = networkRequestInfo;
+                mOldNetworkRequest = oldNetworkRequest;
+                mNewNetworkRequest = newNetworkRequest;
+                mOldNetwork = oldNetwork;
+                mNewNetwork = newNetwork;
+            }
+
+            public String toString() {
+                final NetworkRequest requestToShow = null != mNewNetworkRequest
+                        ? mNewNetworkRequest : mNetworkRequestInfo.mRequests.get(0);
+                return requestToShow.requestId + " : "
+                        + (null != mOldNetwork ? mOldNetwork.network.getNetId() : "null")
+                        + " → " + (null != mNewNetwork ? mNewNetwork.network.getNetId() : "null");
+            }
+        }
+
+        @NonNull private final ArrayList<RequestReassignment> mReassignments = new ArrayList<>();
+
+        @NonNull Iterable<RequestReassignment> getRequestReassignments() {
+            return mReassignments;
+        }
+
+        void addRequestReassignment(@NonNull final RequestReassignment reassignment) {
+            if (Build.isDebuggable()) {
+                // The code is never supposed to add two reassignments of the same request. Make
+                // sure this stays true, but without imposing this expensive check on all
+                // reassignments on all user devices.
+                for (final RequestReassignment existing : mReassignments) {
+                    if (existing.mNetworkRequestInfo.equals(reassignment.mNetworkRequestInfo)) {
+                        throw new IllegalStateException("Trying to reassign ["
+                                + reassignment + "] but already have ["
+                                + existing + "]");
+                    }
+                }
+            }
+            mReassignments.add(reassignment);
+        }
+
+        // Will return null if this reassignment does not change the network assigned to
+        // the passed request.
+        @Nullable
+        private RequestReassignment getReassignment(@NonNull final NetworkRequestInfo nri) {
+            for (final RequestReassignment event : getRequestReassignments()) {
+                if (nri == event.mNetworkRequestInfo) return event;
+            }
+            return null;
+        }
+
+        public String toString() {
+            final StringJoiner sj = new StringJoiner(", " /* delimiter */,
+                    "NetReassign [" /* prefix */, "]" /* suffix */);
+            if (mReassignments.isEmpty()) return sj.add("no changes").toString();
+            for (final RequestReassignment rr : getRequestReassignments()) {
+                sj.add(rr.toString());
+            }
+            return sj.toString();
+        }
+
+        public String debugString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("NetworkReassignment :");
+            if (mReassignments.isEmpty()) return sb.append(" no changes").toString();
+            for (final RequestReassignment rr : getRequestReassignments()) {
+                sb.append("\n  ").append(rr);
+            }
+            return sb.append("\n").toString();
+        }
+    }
+
+    private void updateSatisfiersForRematchRequest(@NonNull final NetworkRequestInfo nri,
+            @Nullable final NetworkRequest previousRequest,
+            @Nullable final NetworkRequest newRequest,
+            @Nullable final NetworkAgentInfo previousSatisfier,
+            @Nullable final NetworkAgentInfo newSatisfier,
+            final long now) {
+        if (null != newSatisfier && mNoServiceNetwork != newSatisfier) {
+            if (VDBG) log("rematch for " + newSatisfier.toShortString());
+            if (null != previousRequest && null != previousSatisfier) {
+                if (VDBG || DDBG) {
+                    log("   accepting network in place of " + previousSatisfier.toShortString());
+                }
+                previousSatisfier.removeRequest(previousRequest.requestId);
+                previousSatisfier.lingerRequest(previousRequest.requestId, now);
+            } else {
+                if (VDBG || DDBG) log("   accepting network in place of null");
+            }
+
+            // To prevent constantly CPU wake up for nascent timer, if a network comes up
+            // and immediately satisfies a request then remove the timer. This will happen for
+            // all networks except in the case of an underlying network for a VCN.
+            if (newSatisfier.isNascent()) {
+                newSatisfier.unlingerRequest(NetworkRequest.REQUEST_ID_NONE);
+                newSatisfier.unsetInactive();
+            }
+
+            // if newSatisfier is not null, then newRequest may not be null.
+            newSatisfier.unlingerRequest(newRequest.requestId);
+            if (!newSatisfier.addRequest(newRequest)) {
+                Log.wtf(TAG, "BUG: " + newSatisfier.toShortString() + " already has "
+                        + newRequest);
+            }
+        } else if (null != previousRequest && null != previousSatisfier) {
+            if (DBG) {
+                log("Network " + previousSatisfier.toShortString() + " stopped satisfying"
+                        + " request " + previousRequest.requestId);
+            }
+            previousSatisfier.removeRequest(previousRequest.requestId);
+        }
+        nri.setSatisfier(newSatisfier, newRequest);
+    }
+
+    /**
+     * This function is triggered when something can affect what network should satisfy what
+     * request, and it computes the network reassignment from the passed collection of requests to
+     * network match to the one that the system should now have. That data is encoded in an
+     * object that is a list of changes, each of them having an NRI, and old satisfier, and a new
+     * satisfier.
+     *
+     * After the reassignment is computed, it is applied to the state objects.
+     *
+     * @param networkRequests the nri objects to evaluate for possible network reassignment
+     * @return NetworkReassignment listing of proposed network assignment changes
+     */
+    @NonNull
+    private NetworkReassignment computeNetworkReassignment(
+            @NonNull final Collection<NetworkRequestInfo> networkRequests) {
+        final NetworkReassignment changes = new NetworkReassignment();
+
+        // Gather the list of all relevant agents.
+        final ArrayList<NetworkAgentInfo> nais = new ArrayList<>();
+        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+            if (!nai.everConnected) {
+                continue;
+            }
+            nais.add(nai);
+        }
+
+        for (final NetworkRequestInfo nri : networkRequests) {
+            // Non-multilayer listen requests can be ignored.
+            if (!nri.isMultilayerRequest() && nri.mRequests.get(0).isListen()) {
+                continue;
+            }
+            NetworkAgentInfo bestNetwork = null;
+            NetworkRequest bestRequest = null;
+            for (final NetworkRequest req : nri.mRequests) {
+                bestNetwork = mNetworkRanker.getBestNetwork(req, nais, nri.getSatisfier());
+                // Stop evaluating as the highest possible priority request is satisfied.
+                if (null != bestNetwork) {
+                    bestRequest = req;
+                    break;
+                }
+            }
+            if (null == bestNetwork && isDefaultBlocked(nri)) {
+                // Remove default networking if disallowed for managed default requests.
+                bestNetwork = mNoServiceNetwork;
+            }
+            if (nri.getSatisfier() != bestNetwork) {
+                // bestNetwork may be null if no network can satisfy this request.
+                changes.addRequestReassignment(new NetworkReassignment.RequestReassignment(
+                        nri, nri.mActiveRequest, bestRequest, nri.getSatisfier(), bestNetwork));
+            }
+        }
+        return changes;
+    }
+
+    private Set<NetworkRequestInfo> getNrisFromGlobalRequests() {
+        return new HashSet<>(mNetworkRequests.values());
+    }
+
+    /**
+     * Attempt to rematch all Networks with all NetworkRequests.  This may result in Networks
+     * being disconnected.
+     */
+    private void rematchAllNetworksAndRequests() {
+        rematchNetworksAndRequests(getNrisFromGlobalRequests());
+    }
+
+    /**
+     * Attempt to rematch all Networks with given NetworkRequests.  This may result in Networks
+     * being disconnected.
+     */
+    private void rematchNetworksAndRequests(
+            @NonNull final Set<NetworkRequestInfo> networkRequests) {
+        ensureRunningOnConnectivityServiceThread();
+        // TODO: This may be slow, and should be optimized.
+        final long now = SystemClock.elapsedRealtime();
+        final NetworkReassignment changes = computeNetworkReassignment(networkRequests);
+        if (VDBG || DDBG) {
+            log(changes.debugString());
+        } else if (DBG) {
+            log(changes.toString()); // Shorter form, only one line of log
+        }
+        applyNetworkReassignment(changes, now);
+        issueNetworkNeeds();
+    }
+
+    private void applyNetworkReassignment(@NonNull final NetworkReassignment changes,
+            final long now) {
+        final Collection<NetworkAgentInfo> nais = mNetworkAgentInfos;
+
+        // Since most of the time there are only 0 or 1 background networks, it would probably
+        // be more efficient to just use an ArrayList here. TODO : measure performance
+        final ArraySet<NetworkAgentInfo> oldBgNetworks = new ArraySet<>();
+        for (final NetworkAgentInfo nai : nais) {
+            if (nai.isBackgroundNetwork()) oldBgNetworks.add(nai);
+        }
+
+        // First, update the lists of satisfied requests in the network agents. This is necessary
+        // because some code later depends on this state to be correct, most prominently computing
+        // the linger status.
+        for (final NetworkReassignment.RequestReassignment event :
+                changes.getRequestReassignments()) {
+            updateSatisfiersForRematchRequest(event.mNetworkRequestInfo,
+                    event.mOldNetworkRequest, event.mNewNetworkRequest,
+                    event.mOldNetwork, event.mNewNetwork,
+                    now);
+        }
+
+        // Process default network changes if applicable.
+        processDefaultNetworkChanges(changes);
+
+        // Notify requested networks are available after the default net is switched, but
+        // before LegacyTypeTracker sends legacy broadcasts
+        for (final NetworkReassignment.RequestReassignment event :
+                changes.getRequestReassignments()) {
+            if (null != event.mNewNetwork) {
+                notifyNetworkAvailable(event.mNewNetwork, event.mNetworkRequestInfo);
+            } else {
+                callCallbackForRequest(event.mNetworkRequestInfo, event.mOldNetwork,
+                        ConnectivityManager.CALLBACK_LOST, 0);
+            }
+        }
+
+        // Update the inactivity state before processing listen callbacks, because the background
+        // computation depends on whether the network is inactive. Don't send the LOSING callbacks
+        // just yet though, because they have to be sent after the listens are processed to keep
+        // backward compatibility.
+        final ArrayList<NetworkAgentInfo> inactiveNetworks = new ArrayList<>();
+        for (final NetworkAgentInfo nai : nais) {
+            // Rematching may have altered the inactivity state of some networks, so update all
+            // inactivity timers. updateInactivityState reads the state from the network agent
+            // and does nothing if the state has not changed : the source of truth is controlled
+            // with NetworkAgentInfo#lingerRequest and NetworkAgentInfo#unlingerRequest, which
+            // have been called while rematching the individual networks above.
+            if (updateInactivityState(nai, now)) {
+                inactiveNetworks.add(nai);
+            }
+        }
+
+        for (final NetworkAgentInfo nai : nais) {
+            if (!nai.everConnected) continue;
+            final boolean oldBackground = oldBgNetworks.contains(nai);
+            // Process listen requests and update capabilities if the background state has
+            // changed for this network. For consistency with previous behavior, send onLost
+            // callbacks before onAvailable.
+            processNewlyLostListenRequests(nai);
+            if (oldBackground != nai.isBackgroundNetwork()) {
+                applyBackgroundChangeForRematch(nai);
+            }
+            processNewlySatisfiedListenRequests(nai);
+        }
+
+        for (final NetworkAgentInfo nai : inactiveNetworks) {
+            // For nascent networks, if connecting with no foreground request, skip broadcasting
+            // LOSING for backward compatibility. This is typical when mobile data connected while
+            // wifi connected with mobile data always-on enabled.
+            if (nai.isNascent()) continue;
+            notifyNetworkLosing(nai, now);
+        }
+
+        updateLegacyTypeTrackerAndVpnLockdownForRematch(changes, nais);
+
+        // Tear down all unneeded networks.
+        for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+            if (unneeded(nai, UnneededFor.TEARDOWN)) {
+                if (nai.getInactivityExpiry() > 0) {
+                    // This network has active linger timers and no requests, but is not
+                    // lingering. Linger it.
+                    //
+                    // One way (the only way?) this can happen if this network is unvalidated
+                    // and became unneeded due to another network improving its score to the
+                    // point where this network will no longer be able to satisfy any requests
+                    // even if it validates.
+                    if (updateInactivityState(nai, now)) {
+                        notifyNetworkLosing(nai, now);
+                    }
+                } else {
+                    if (DBG) log("Reaping " + nai.toShortString());
+                    teardownUnneededNetwork(nai);
+                }
+            }
+        }
+    }
+
+    /**
+     * Apply a change in background state resulting from rematching networks with requests.
+     *
+     * During rematch, a network may change background states by starting to satisfy or stopping
+     * to satisfy a foreground request. Listens don't count for this. When a network changes
+     * background states, its capabilities need to be updated and callbacks fired for the
+     * capability change.
+     *
+     * @param nai The network that changed background states
+     */
+    private void applyBackgroundChangeForRematch(@NonNull final NetworkAgentInfo nai) {
+        final NetworkCapabilities newNc = mixInCapabilities(nai, nai.networkCapabilities);
+        if (Objects.equals(nai.networkCapabilities, newNc)) return;
+        updateNetworkPermissions(nai, newNc);
+        nai.getAndSetNetworkCapabilities(newNc);
+        notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+    }
+
+    private void updateLegacyTypeTrackerAndVpnLockdownForRematch(
+            @NonNull final NetworkReassignment changes,
+            @NonNull final Collection<NetworkAgentInfo> nais) {
+        final NetworkReassignment.RequestReassignment reassignmentOfDefault =
+                changes.getReassignment(mDefaultRequest);
+        final NetworkAgentInfo oldDefaultNetwork =
+                null != reassignmentOfDefault ? reassignmentOfDefault.mOldNetwork : null;
+        final NetworkAgentInfo newDefaultNetwork =
+                null != reassignmentOfDefault ? reassignmentOfDefault.mNewNetwork : null;
+
+        if (oldDefaultNetwork != newDefaultNetwork) {
+            // Maintain the illusion : since the legacy API only understands one network at a time,
+            // if the default network changed, apps should see a disconnected broadcast for the
+            // old default network before they see a connected broadcast for the new one.
+            if (oldDefaultNetwork != null) {
+                mLegacyTypeTracker.remove(oldDefaultNetwork.networkInfo.getType(),
+                        oldDefaultNetwork, true);
+            }
+            if (newDefaultNetwork != null) {
+                // The new default network can be newly null if and only if the old default
+                // network doesn't satisfy the default request any more because it lost a
+                // capability.
+                mDefaultInetConditionPublished = newDefaultNetwork.lastValidated ? 100 : 0;
+                mLegacyTypeTracker.add(
+                        newDefaultNetwork.networkInfo.getType(), newDefaultNetwork);
+            }
+        }
+
+        // Now that all the callbacks have been sent, send the legacy network broadcasts
+        // as needed. This is necessary so that legacy requests correctly bind dns
+        // requests to this network. The legacy users are listening for this broadcast
+        // and will generally do a dns request so they can ensureRouteToHost and if
+        // they do that before the callbacks happen they'll use the default network.
+        //
+        // TODO: Is there still a race here? The legacy broadcast will be sent after sending
+        // callbacks, but if apps can receive the broadcast before the callback, they still might
+        // have an inconsistent view of networking.
+        //
+        // This *does* introduce a race where if the user uses the new api
+        // (notification callbacks) and then uses the old api (getNetworkInfo(type))
+        // they may get old info. Reverse this after the old startUsing api is removed.
+        // This is on top of the multiple intent sequencing referenced in the todo above.
+        for (NetworkAgentInfo nai : nais) {
+            if (nai.everConnected) {
+                addNetworkToLegacyTypeTracker(nai);
+            }
+        }
+    }
+
+    private void issueNetworkNeeds() {
+        ensureRunningOnConnectivityServiceThread();
+        for (final NetworkOfferInfo noi : mNetworkOffers) {
+            issueNetworkNeeds(noi);
+        }
+    }
+
+    private void issueNetworkNeeds(@NonNull final NetworkOfferInfo noi) {
+        ensureRunningOnConnectivityServiceThread();
+        for (final NetworkRequestInfo nri : mNetworkRequests.values()) {
+            informOffer(nri, noi.offer, mNetworkRanker);
+        }
+    }
+
+    /**
+     * Inform a NetworkOffer about any new situation of a request.
+     *
+     * This function handles updates to offers. A number of events may happen that require
+     * updating the registrant for this offer about the situation :
+     * • The offer itself was updated. This may lead the offer to no longer being able
+     *     to satisfy a request or beat a satisfier (and therefore be no longer needed),
+     *     or conversely being strengthened enough to beat the satisfier (and therefore
+     *     start being needed)
+     * • The network satisfying a request changed (including cases where the request
+     *     starts or stops being satisfied). The new network may be a stronger or weaker
+     *     match than the old one, possibly affecting whether the offer is needed.
+     * • The network satisfying a request updated their score. This may lead the offer
+     *     to no longer be able to beat it if the current satisfier got better, or
+     *     conversely start being a good choice if the current satisfier got weaker.
+     *
+     * @param nri The request
+     * @param offer The offer. This may be an updated offer.
+     */
+    private static void informOffer(@NonNull NetworkRequestInfo nri,
+            @NonNull final NetworkOffer offer, @NonNull final NetworkRanker networkRanker) {
+        final NetworkRequest activeRequest = nri.isBeingSatisfied() ? nri.getActiveRequest() : null;
+        final NetworkAgentInfo satisfier = null != activeRequest ? nri.getSatisfier() : null;
+
+        // Multi-layer requests have a currently active request, the one being satisfied.
+        // Since the system will try to bring up a better network than is currently satisfying
+        // the request, NetworkProviders need to be told the offers matching the requests *above*
+        // the currently satisfied one are needed, that the ones *below* the satisfied one are
+        // not needed, and the offer is needed for the active request iff the offer can beat
+        // the satisfier.
+        // For non-multilayer requests, the logic above gracefully degenerates to only the
+        // last case.
+        // To achieve this, the loop below will proceed in three steps. In a first phase, inform
+        // providers that the offer is needed for this request, until the active request is found.
+        // In a second phase, deal with the currently active request. In a third phase, inform
+        // the providers that offer is unneeded for the remaining requests.
+
+        // First phase : inform providers of all requests above the active request.
+        int i;
+        for (i = 0; nri.mRequests.size() > i; ++i) {
+            final NetworkRequest request = nri.mRequests.get(i);
+            if (activeRequest == request) break; // Found the active request : go to phase 2
+            if (!request.isRequest()) continue; // Listens/track defaults are never sent to offers
+            // Since this request is higher-priority than the one currently satisfied, if the
+            // offer can satisfy it, the provider should try and bring up the network for sure ;
+            // no need to even ask the ranker – an offer that can satisfy is always better than
+            // no network. Hence tell the provider so unless it already knew.
+            if (request.canBeSatisfiedBy(offer.caps) && !offer.neededFor(request)) {
+                offer.onNetworkNeeded(request);
+            }
+        }
+
+        // Second phase : deal with the active request (if any)
+        if (null != activeRequest && activeRequest.isRequest()) {
+            final boolean oldNeeded = offer.neededFor(activeRequest);
+            // An offer is needed if it is currently served by this provider or if this offer
+            // can beat the current satisfier.
+            final boolean currentlyServing = satisfier != null
+                    && satisfier.factorySerialNumber == offer.providerId;
+            final boolean newNeeded = (currentlyServing
+                    || (activeRequest.canBeSatisfiedBy(offer.caps)
+                            && networkRanker.mightBeat(activeRequest, satisfier, offer)));
+            if (newNeeded != oldNeeded) {
+                if (newNeeded) {
+                    offer.onNetworkNeeded(activeRequest);
+                } else {
+                    // The offer used to be able to beat the satisfier. Now it can't.
+                    offer.onNetworkUnneeded(activeRequest);
+                }
+            }
+        }
+
+        // Third phase : inform the providers that the offer isn't needed for any request
+        // below the active one.
+        for (++i /* skip the active request */; nri.mRequests.size() > i; ++i) {
+            final NetworkRequest request = nri.mRequests.get(i);
+            if (!request.isRequest()) continue; // Listens/track defaults are never sent to offers
+            // Since this request is lower-priority than the one currently satisfied, if the
+            // offer can satisfy it, the provider should not try and bring up the network.
+            // Hence tell the provider so unless it already knew.
+            if (offer.neededFor(request)) {
+                offer.onNetworkUnneeded(request);
+            }
+        }
+    }
+
+    private void addNetworkToLegacyTypeTracker(@NonNull final NetworkAgentInfo nai) {
+        for (int i = 0; i < nai.numNetworkRequests(); i++) {
+            NetworkRequest nr = nai.requestAt(i);
+            if (nr.legacyType != TYPE_NONE && nr.isRequest()) {
+                // legacy type tracker filters out repeat adds
+                mLegacyTypeTracker.add(nr.legacyType, nai);
+            }
+        }
+
+        // A VPN generally won't get added to the legacy tracker in the "for (nri)" loop above,
+        // because usually there are no NetworkRequests it satisfies (e.g., mDefaultRequest
+        // wants the NOT_VPN capability, so it will never be satisfied by a VPN). So, add the
+        // newNetwork to the tracker explicitly (it's a no-op if it has already been added).
+        if (nai.isVPN()) {
+            mLegacyTypeTracker.add(TYPE_VPN, nai);
+        }
+    }
+
+    private void updateInetCondition(NetworkAgentInfo nai) {
+        // Don't bother updating until we've graduated to validated at least once.
+        if (!nai.everValidated) return;
+        // For now only update icons for the default connection.
+        // TODO: Update WiFi and cellular icons separately. b/17237507
+        if (!isDefaultNetwork(nai)) return;
+
+        int newInetCondition = nai.lastValidated ? 100 : 0;
+        // Don't repeat publish.
+        if (newInetCondition == mDefaultInetConditionPublished) return;
+
+        mDefaultInetConditionPublished = newInetCondition;
+        sendInetConditionBroadcast(nai.networkInfo);
+    }
+
+    @NonNull
+    private NetworkInfo mixInInfo(@NonNull final NetworkAgentInfo nai, @NonNull NetworkInfo info) {
+        final NetworkInfo newInfo = new NetworkInfo(info);
+        // The suspended and roaming bits are managed in NetworkCapabilities.
+        final boolean suspended =
+                !nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        if (suspended && info.getDetailedState() == NetworkInfo.DetailedState.CONNECTED) {
+            // Only override the state with SUSPENDED if the network is currently in CONNECTED
+            // state. This is because the network could have been suspended before connecting,
+            // or it could be disconnecting while being suspended, and in both these cases
+            // the state should not be overridden. Note that the only detailed state that
+            // maps to State.CONNECTED is DetailedState.CONNECTED, so there is also no need to
+            // worry about multiple different substates of CONNECTED.
+            newInfo.setDetailedState(NetworkInfo.DetailedState.SUSPENDED, info.getReason(),
+                    info.getExtraInfo());
+        } else if (!suspended && info.getDetailedState() == NetworkInfo.DetailedState.SUSPENDED) {
+            // SUSPENDED state is currently only overridden from CONNECTED state. In the case the
+            // network agent is created, then goes to suspended, then goes out of suspended without
+            // ever setting connected. Check if network agent is ever connected to update the state.
+            newInfo.setDetailedState(nai.everConnected
+                    ? NetworkInfo.DetailedState.CONNECTED
+                    : NetworkInfo.DetailedState.CONNECTING,
+                    info.getReason(),
+                    info.getExtraInfo());
+        }
+        newInfo.setRoaming(!nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING));
+        return newInfo;
+    }
+
+    private void updateNetworkInfo(NetworkAgentInfo networkAgent, NetworkInfo info) {
+        final NetworkInfo newInfo = mixInInfo(networkAgent, info);
+
+        final NetworkInfo.State state = newInfo.getState();
+        NetworkInfo oldInfo = null;
+        synchronized (networkAgent) {
+            oldInfo = networkAgent.networkInfo;
+            networkAgent.networkInfo = newInfo;
+        }
+
+        if (DBG) {
+            log(networkAgent.toShortString() + " EVENT_NETWORK_INFO_CHANGED, going from "
+                    + oldInfo.getState() + " to " + state);
+        }
+
+        if (!networkAgent.created
+                && (state == NetworkInfo.State.CONNECTED
+                || (state == NetworkInfo.State.CONNECTING && networkAgent.isVPN()))) {
+
+            // A network that has just connected has zero requests and is thus a foreground network.
+            networkAgent.networkCapabilities.addCapability(NET_CAPABILITY_FOREGROUND);
+
+            if (!createNativeNetwork(networkAgent)) return;
+            if (networkAgent.supportsUnderlyingNetworks()) {
+                // Initialize the network's capabilities to their starting values according to the
+                // underlying networks. This ensures that the capabilities are correct before
+                // anything happens to the network.
+                updateCapabilitiesForNetwork(networkAgent);
+            }
+            networkAgent.created = true;
+            networkAgent.onNetworkCreated();
+        }
+
+        if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) {
+            networkAgent.everConnected = true;
+
+            // NetworkCapabilities need to be set before sending the private DNS config to
+            // NetworkMonitor, otherwise NetworkMonitor cannot determine if validation is required.
+            networkAgent.getAndSetNetworkCapabilities(networkAgent.networkCapabilities);
+
+            handlePerNetworkPrivateDnsConfig(networkAgent, mDnsManager.getPrivateDnsConfig());
+            updateLinkProperties(networkAgent, new LinkProperties(networkAgent.linkProperties),
+                    null);
+
+            // Until parceled LinkProperties are sent directly to NetworkMonitor, the connect
+            // command must be sent after updating LinkProperties to maximize chances of
+            // NetworkMonitor seeing the correct LinkProperties when starting.
+            // TODO: pass LinkProperties to the NetworkMonitor in the notifyNetworkConnected call.
+            if (networkAgent.networkAgentConfig.acceptPartialConnectivity) {
+                networkAgent.networkMonitor().setAcceptPartialConnectivity();
+            }
+            networkAgent.networkMonitor().notifyNetworkConnected(
+                    new LinkProperties(networkAgent.linkProperties,
+                            true /* parcelSensitiveFields */),
+                    networkAgent.networkCapabilities);
+            scheduleUnvalidatedPrompt(networkAgent);
+
+            // Whether a particular NetworkRequest listen should cause signal strength thresholds to
+            // be communicated to a particular NetworkAgent depends only on the network's immutable,
+            // capabilities, so it only needs to be done once on initial connect, not every time the
+            // network's capabilities change. Note that we do this before rematching the network,
+            // so we could decide to tear it down immediately afterwards. That's fine though - on
+            // disconnection NetworkAgents should stop any signal strength monitoring they have been
+            // doing.
+            updateSignalStrengthThresholds(networkAgent, "CONNECT", null);
+
+            // Before first rematching networks, put an inactivity timer without any request, this
+            // allows {@code updateInactivityState} to update the state accordingly and prevent
+            // tearing down for any {@code unneeded} evaluation in this period.
+            // Note that the timer will not be rescheduled since the expiry time is
+            // fixed after connection regardless of the network satisfying other requests or not.
+            // But it will be removed as soon as the network satisfies a request for the first time.
+            networkAgent.lingerRequest(NetworkRequest.REQUEST_ID_NONE,
+                    SystemClock.elapsedRealtime(), mNascentDelayMs);
+            networkAgent.setInactive();
+
+            // Consider network even though it is not yet validated.
+            rematchAllNetworksAndRequests();
+
+            // This has to happen after matching the requests, because callbacks are just requests.
+            notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_PRECHECK);
+        } else if (state == NetworkInfo.State.DISCONNECTED) {
+            networkAgent.disconnect();
+            if (networkAgent.isVPN()) {
+                updateUids(networkAgent, networkAgent.networkCapabilities, null);
+            }
+            disconnectAndDestroyNetwork(networkAgent);
+            if (networkAgent.isVPN()) {
+                // As the active or bound network changes for apps, broadcast the default proxy, as
+                // apps may need to update their proxy data. This is called after disconnecting from
+                // VPN to make sure we do not broadcast the old proxy data.
+                // TODO(b/122649188): send the broadcast only to VPN users.
+                mProxyTracker.sendProxyBroadcast();
+            }
+        } else if (networkAgent.created && (oldInfo.getState() == NetworkInfo.State.SUSPENDED ||
+                state == NetworkInfo.State.SUSPENDED)) {
+            mLegacyTypeTracker.update(networkAgent);
+        }
+    }
+
+    private void updateNetworkScore(@NonNull final NetworkAgentInfo nai, final NetworkScore score) {
+        if (VDBG || DDBG) log("updateNetworkScore for " + nai.toShortString() + " to " + score);
+        nai.setScore(score);
+        rematchAllNetworksAndRequests();
+    }
+
+    // Notify only this one new request of the current state. Transfer all the
+    // current state by calling NetworkCapabilities and LinkProperties callbacks
+    // so that callers can be guaranteed to have as close to atomicity in state
+    // transfer as can be supported by this current API.
+    protected void notifyNetworkAvailable(NetworkAgentInfo nai, NetworkRequestInfo nri) {
+        mHandler.removeMessages(EVENT_TIMEOUT_NETWORK_REQUEST, nri);
+        if (nri.mPendingIntent != null) {
+            sendPendingIntentForRequest(nri, nai, ConnectivityManager.CALLBACK_AVAILABLE);
+            // Attempt no subsequent state pushes where intents are involved.
+            return;
+        }
+
+        final int blockedReasons = mUidBlockedReasons.get(nri.mAsUid, BLOCKED_REASON_NONE);
+        final boolean metered = nai.networkCapabilities.isMetered();
+        final boolean vpnBlocked = isUidBlockedByVpn(nri.mAsUid, mVpnBlockedUidRanges);
+        callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_AVAILABLE,
+                getBlockedState(blockedReasons, metered, vpnBlocked));
+    }
+
+    // Notify the requests on this NAI that the network is now lingered.
+    private void notifyNetworkLosing(@NonNull final NetworkAgentInfo nai, final long now) {
+        final int lingerTime = (int) (nai.getInactivityExpiry() - now);
+        notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING, lingerTime);
+    }
+
+    private static int getBlockedState(int reasons, boolean metered, boolean vpnBlocked) {
+        if (!metered) reasons &= ~BLOCKED_METERED_REASON_MASK;
+        return vpnBlocked
+                ? reasons | BLOCKED_REASON_LOCKDOWN_VPN
+                : reasons & ~BLOCKED_REASON_LOCKDOWN_VPN;
+    }
+
+    private void setUidBlockedReasons(int uid, @BlockedReason int blockedReasons) {
+        if (blockedReasons == BLOCKED_REASON_NONE) {
+            mUidBlockedReasons.delete(uid);
+        } else {
+            mUidBlockedReasons.put(uid, blockedReasons);
+        }
+    }
+
+    /**
+     * Notify of the blocked state apps with a registered callback matching a given NAI.
+     *
+     * Unlike other callbacks, blocked status is different between each individual uid. So for
+     * any given nai, all requests need to be considered according to the uid who filed it.
+     *
+     * @param nai The target NetworkAgentInfo.
+     * @param oldMetered True if the previous network capabilities were metered.
+     * @param newMetered True if the current network capabilities are metered.
+     * @param oldBlockedUidRanges list of UID ranges previously blocked by lockdown VPN.
+     * @param newBlockedUidRanges list of UID ranges blocked by lockdown VPN.
+     */
+    private void maybeNotifyNetworkBlocked(NetworkAgentInfo nai, boolean oldMetered,
+            boolean newMetered, List<UidRange> oldBlockedUidRanges,
+            List<UidRange> newBlockedUidRanges) {
+
+        for (int i = 0; i < nai.numNetworkRequests(); i++) {
+            NetworkRequest nr = nai.requestAt(i);
+            NetworkRequestInfo nri = mNetworkRequests.get(nr);
+
+            final int blockedReasons = mUidBlockedReasons.get(nri.mAsUid, BLOCKED_REASON_NONE);
+            final boolean oldVpnBlocked = isUidBlockedByVpn(nri.mAsUid, oldBlockedUidRanges);
+            final boolean newVpnBlocked = (oldBlockedUidRanges != newBlockedUidRanges)
+                    ? isUidBlockedByVpn(nri.mAsUid, newBlockedUidRanges)
+                    : oldVpnBlocked;
+
+            final int oldBlockedState = getBlockedState(blockedReasons, oldMetered, oldVpnBlocked);
+            final int newBlockedState = getBlockedState(blockedReasons, newMetered, newVpnBlocked);
+            if (oldBlockedState != newBlockedState) {
+                callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_BLK_CHANGED,
+                        newBlockedState);
+            }
+        }
+    }
+
+    /**
+     * Notify apps with a given UID of the new blocked state according to new uid state.
+     * @param uid The uid for which the rules changed.
+     * @param blockedReasons The reasons for why an uid is blocked.
+     */
+    private void maybeNotifyNetworkBlockedForNewState(int uid, @BlockedReason int blockedReasons) {
+        for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+            final boolean metered = nai.networkCapabilities.isMetered();
+            final boolean vpnBlocked = isUidBlockedByVpn(uid, mVpnBlockedUidRanges);
+
+            final int oldBlockedState = getBlockedState(
+                    mUidBlockedReasons.get(uid, BLOCKED_REASON_NONE), metered, vpnBlocked);
+            final int newBlockedState = getBlockedState(blockedReasons, metered, vpnBlocked);
+            if (oldBlockedState == newBlockedState) {
+                continue;
+            }
+            for (int i = 0; i < nai.numNetworkRequests(); i++) {
+                NetworkRequest nr = nai.requestAt(i);
+                NetworkRequestInfo nri = mNetworkRequests.get(nr);
+                if (nri != null && nri.mAsUid == uid) {
+                    callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_BLK_CHANGED,
+                            newBlockedState);
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    protected void sendLegacyNetworkBroadcast(NetworkAgentInfo nai, DetailedState state, int type) {
+        // The NetworkInfo we actually send out has no bearing on the real
+        // state of affairs. For example, if the default connection is mobile,
+        // and a request for HIPRI has just gone away, we need to pretend that
+        // HIPRI has just disconnected. So we need to set the type to HIPRI and
+        // the state to DISCONNECTED, even though the network is of type MOBILE
+        // and is still connected.
+        NetworkInfo info = new NetworkInfo(nai.networkInfo);
+        info.setType(type);
+        filterForLegacyLockdown(info);
+        if (state != DetailedState.DISCONNECTED) {
+            info.setDetailedState(state, null, info.getExtraInfo());
+            sendConnectedBroadcast(info);
+        } else {
+            info.setDetailedState(state, info.getReason(), info.getExtraInfo());
+            Intent intent = new Intent(ConnectivityManager.CONNECTIVITY_ACTION);
+            intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, info);
+            intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType());
+            if (info.isFailover()) {
+                intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true);
+                nai.networkInfo.setFailover(false);
+            }
+            if (info.getReason() != null) {
+                intent.putExtra(ConnectivityManager.EXTRA_REASON, info.getReason());
+            }
+            if (info.getExtraInfo() != null) {
+                intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO, info.getExtraInfo());
+            }
+            NetworkAgentInfo newDefaultAgent = null;
+            if (nai.isSatisfyingRequest(mDefaultRequest.mRequests.get(0).requestId)) {
+                newDefaultAgent = mDefaultRequest.getSatisfier();
+                if (newDefaultAgent != null) {
+                    intent.putExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO,
+                            newDefaultAgent.networkInfo);
+                } else {
+                    intent.putExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, true);
+                }
+            }
+            intent.putExtra(ConnectivityManager.EXTRA_INET_CONDITION,
+                    mDefaultInetConditionPublished);
+            sendStickyBroadcast(intent);
+            if (newDefaultAgent != null) {
+                sendConnectedBroadcast(newDefaultAgent.networkInfo);
+            }
+        }
+    }
+
+    protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType, int arg1) {
+        if (VDBG || DDBG) {
+            String notification = ConnectivityManager.getCallbackName(notifyType);
+            log("notifyType " + notification + " for " + networkAgent.toShortString());
+        }
+        for (int i = 0; i < networkAgent.numNetworkRequests(); i++) {
+            NetworkRequest nr = networkAgent.requestAt(i);
+            NetworkRequestInfo nri = mNetworkRequests.get(nr);
+            if (VDBG) log(" sending notification for " + nr);
+            if (nri.mPendingIntent == null) {
+                callCallbackForRequest(nri, networkAgent, notifyType, arg1);
+            } else {
+                sendPendingIntentForRequest(nri, networkAgent, notifyType);
+            }
+        }
+    }
+
+    protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType) {
+        notifyNetworkCallbacks(networkAgent, notifyType, 0);
+    }
+
+    /**
+     * Returns the list of all interfaces that could be used by network traffic that does not
+     * explicitly specify a network. This includes the default network, but also all VPNs that are
+     * currently connected.
+     *
+     * Must be called on the handler thread.
+     */
+    @NonNull
+    private ArrayList<Network> getDefaultNetworks() {
+        ensureRunningOnConnectivityServiceThread();
+        final ArrayList<Network> defaultNetworks = new ArrayList<>();
+        final Set<Integer> activeNetIds = new ArraySet<>();
+        for (final NetworkRequestInfo nri : mDefaultNetworkRequests) {
+            if (nri.isBeingSatisfied()) {
+                activeNetIds.add(nri.getSatisfier().network().netId);
+            }
+        }
+        for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+            if (nai.everConnected && (activeNetIds.contains(nai.network().netId) || nai.isVPN())) {
+                defaultNetworks.add(nai.network);
+            }
+        }
+        return defaultNetworks;
+    }
+
+    /**
+     * Notify NetworkStatsService that the set of active ifaces has changed, or that one of the
+     * active iface's tracked properties has changed.
+     */
+    private void notifyIfacesChangedForNetworkStats() {
+        ensureRunningOnConnectivityServiceThread();
+        String activeIface = null;
+        LinkProperties activeLinkProperties = getActiveLinkProperties();
+        if (activeLinkProperties != null) {
+            activeIface = activeLinkProperties.getInterfaceName();
+        }
+
+        final UnderlyingNetworkInfo[] underlyingNetworkInfos = getAllVpnInfo();
+        try {
+            final ArrayList<NetworkStateSnapshot> snapshots = new ArrayList<>();
+            for (final NetworkStateSnapshot snapshot : getAllNetworkStateSnapshots()) {
+                snapshots.add(snapshot);
+            }
+            mStatsManager.notifyNetworkStatus(getDefaultNetworks(),
+                    snapshots, activeIface, Arrays.asList(underlyingNetworkInfos));
+        } catch (Exception ignored) {
+        }
+    }
+
+    @Override
+    public String getCaptivePortalServerUrl() {
+        enforceNetworkStackOrSettingsPermission();
+        String settingUrl = mResources.get().getString(
+                R.string.config_networkCaptivePortalServerUrl);
+
+        if (!TextUtils.isEmpty(settingUrl)) {
+            return settingUrl;
+        }
+
+        settingUrl = Settings.Global.getString(mContext.getContentResolver(),
+                ConnectivitySettingsManager.CAPTIVE_PORTAL_HTTP_URL);
+        if (!TextUtils.isEmpty(settingUrl)) {
+            return settingUrl;
+        }
+
+        return DEFAULT_CAPTIVE_PORTAL_HTTP_URL;
+    }
+
+    @Override
+    public void startNattKeepalive(Network network, int intervalSeconds,
+            ISocketKeepaliveCallback cb, String srcAddr, int srcPort, String dstAddr) {
+        enforceKeepalivePermission();
+        mKeepaliveTracker.startNattKeepalive(
+                getNetworkAgentInfoForNetwork(network), null /* fd */,
+                intervalSeconds, cb,
+                srcAddr, srcPort, dstAddr, NattSocketKeepalive.NATT_PORT);
+    }
+
+    @Override
+    public void startNattKeepaliveWithFd(Network network, ParcelFileDescriptor pfd, int resourceId,
+            int intervalSeconds, ISocketKeepaliveCallback cb, String srcAddr,
+            String dstAddr) {
+        try {
+            final FileDescriptor fd = pfd.getFileDescriptor();
+            mKeepaliveTracker.startNattKeepalive(
+                    getNetworkAgentInfoForNetwork(network), fd, resourceId,
+                    intervalSeconds, cb,
+                    srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT);
+        } finally {
+            // FileDescriptors coming from AIDL calls must be manually closed to prevent leaks.
+            // startNattKeepalive calls Os.dup(fd) before returning, so we can close immediately.
+            if (pfd != null && Binder.getCallingPid() != Process.myPid()) {
+                IoUtils.closeQuietly(pfd);
+            }
+        }
+    }
+
+    @Override
+    public void startTcpKeepalive(Network network, ParcelFileDescriptor pfd, int intervalSeconds,
+            ISocketKeepaliveCallback cb) {
+        try {
+            enforceKeepalivePermission();
+            final FileDescriptor fd = pfd.getFileDescriptor();
+            mKeepaliveTracker.startTcpKeepalive(
+                    getNetworkAgentInfoForNetwork(network), fd, intervalSeconds, cb);
+        } finally {
+            // FileDescriptors coming from AIDL calls must be manually closed to prevent leaks.
+            // startTcpKeepalive calls Os.dup(fd) before returning, so we can close immediately.
+            if (pfd != null && Binder.getCallingPid() != Process.myPid()) {
+                IoUtils.closeQuietly(pfd);
+            }
+        }
+    }
+
+    @Override
+    public void stopKeepalive(Network network, int slot) {
+        mHandler.sendMessage(mHandler.obtainMessage(
+                NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE, slot, SocketKeepalive.SUCCESS, network));
+    }
+
+    @Override
+    public void factoryReset() {
+        enforceSettingsPermission();
+
+        final int uid = mDeps.getCallingUid();
+        final long token = Binder.clearCallingIdentity();
+        try {
+            if (mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_NETWORK_RESET,
+                    UserHandle.getUserHandleForUid(uid))) {
+                return;
+            }
+
+            final IpMemoryStore ipMemoryStore = IpMemoryStore.getMemoryStore(mContext);
+            ipMemoryStore.factoryReset();
+
+            // Turn airplane mode off
+            setAirplaneMode(false);
+
+            // restore private DNS settings to default mode (opportunistic)
+            if (!mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_CONFIG_PRIVATE_DNS,
+                    UserHandle.getUserHandleForUid(uid))) {
+                ConnectivitySettingsManager.setPrivateDnsMode(mContext,
+                        PRIVATE_DNS_MODE_OPPORTUNISTIC);
+            }
+
+            Settings.Global.putString(mContext.getContentResolver(),
+                    ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, null);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public byte[] getNetworkWatchlistConfigHash() {
+        NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);
+        if (nwm == null) {
+            loge("Unable to get NetworkWatchlistManager");
+            return null;
+        }
+        // Redirect it to network watchlist service to access watchlist file and calculate hash.
+        return nwm.getWatchlistConfigHash();
+    }
+
+    private void logNetworkEvent(NetworkAgentInfo nai, int evtype) {
+        int[] transports = nai.networkCapabilities.getTransportTypes();
+        mMetricsLog.log(nai.network.getNetId(), transports, new NetworkEvent(evtype));
+    }
+
+    private static boolean toBool(int encodedBoolean) {
+        return encodedBoolean != 0; // Only 0 means false.
+    }
+
+    private static int encodeBool(boolean b) {
+        return b ? 1 : 0;
+    }
+
+    @Override
+    public int handleShellCommand(@NonNull ParcelFileDescriptor in,
+            @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
+            @NonNull String[] args) {
+        return new ShellCmd().exec(this, in.getFileDescriptor(), out.getFileDescriptor(),
+                err.getFileDescriptor(), args);
+    }
+
+    private class ShellCmd extends BasicShellCommandHandler {
+        @Override
+        public int onCommand(String cmd) {
+            if (cmd == null) {
+                return handleDefaultCommands(cmd);
+            }
+            final PrintWriter pw = getOutPrintWriter();
+            try {
+                switch (cmd) {
+                    case "airplane-mode":
+                        final String action = getNextArg();
+                        if ("enable".equals(action)) {
+                            setAirplaneMode(true);
+                            return 0;
+                        } else if ("disable".equals(action)) {
+                            setAirplaneMode(false);
+                            return 0;
+                        } else if (action == null) {
+                            final ContentResolver cr = mContext.getContentResolver();
+                            final int enabled = Settings.Global.getInt(cr,
+                                    Settings.Global.AIRPLANE_MODE_ON);
+                            pw.println(enabled == 0 ? "disabled" : "enabled");
+                            return 0;
+                        } else {
+                            onHelp();
+                            return -1;
+                        }
+                    default:
+                        return handleDefaultCommands(cmd);
+                }
+            } catch (Exception e) {
+                pw.println(e);
+            }
+            return -1;
+        }
+
+        @Override
+        public void onHelp() {
+            PrintWriter pw = getOutPrintWriter();
+            pw.println("Connectivity service commands:");
+            pw.println("  help");
+            pw.println("    Print this help text.");
+            pw.println("  airplane-mode [enable|disable]");
+            pw.println("    Turn airplane mode on or off.");
+            pw.println("  airplane-mode");
+            pw.println("    Get airplane mode.");
+        }
+    }
+
+    private int getVpnType(@Nullable NetworkAgentInfo vpn) {
+        if (vpn == null) return VpnManager.TYPE_VPN_NONE;
+        final TransportInfo ti = vpn.networkCapabilities.getTransportInfo();
+        if (!(ti instanceof VpnTransportInfo)) return VpnManager.TYPE_VPN_NONE;
+        return ((VpnTransportInfo) ti).getType();
+    }
+
+    /**
+     * @param connectionInfo the connection to resolve.
+     * @return {@code uid} if the connection is found and the app has permission to observe it
+     * (e.g., if it is associated with the calling VPN app's tunnel) or {@code INVALID_UID} if the
+     * connection is not found.
+     */
+    public int getConnectionOwnerUid(ConnectionInfo connectionInfo) {
+        if (connectionInfo.protocol != IPPROTO_TCP && connectionInfo.protocol != IPPROTO_UDP) {
+            throw new IllegalArgumentException("Unsupported protocol " + connectionInfo.protocol);
+        }
+
+        final int uid = mDeps.getConnectionOwnerUid(connectionInfo.protocol,
+                connectionInfo.local, connectionInfo.remote);
+
+        if (uid == INVALID_UID) return uid;  // Not found.
+
+        // Connection owner UIDs are visible only to the network stack and to the VpnService-based
+        // VPN, if any, that applies to the UID that owns the connection.
+        if (checkNetworkStackPermission()) return uid;
+
+        final NetworkAgentInfo vpn = getVpnForUid(uid);
+        if (vpn == null || getVpnType(vpn) != VpnManager.TYPE_VPN_SERVICE
+                || vpn.networkCapabilities.getOwnerUid() != mDeps.getCallingUid()) {
+            return INVALID_UID;
+        }
+
+        return uid;
+    }
+
+    /**
+     * Returns a IBinder to a TestNetworkService. Will be lazily created as needed.
+     *
+     * <p>The TestNetworkService must be run in the system server due to TUN creation.
+     */
+    @Override
+    public IBinder startOrGetTestNetworkService() {
+        synchronized (mTNSLock) {
+            TestNetworkService.enforceTestNetworkPermissions(mContext);
+
+            if (mTNS == null) {
+                mTNS = new TestNetworkService(mContext);
+            }
+
+            return mTNS;
+        }
+    }
+
+    /**
+     * Handler used for managing all Connectivity Diagnostics related functions.
+     *
+     * @see android.net.ConnectivityDiagnosticsManager
+     *
+     * TODO(b/147816404): Explore moving ConnectivityDiagnosticsHandler to a separate file
+     */
+    @VisibleForTesting
+    class ConnectivityDiagnosticsHandler extends Handler {
+        private final String mTag = ConnectivityDiagnosticsHandler.class.getSimpleName();
+
+        /**
+         * Used to handle ConnectivityDiagnosticsCallback registration events from {@link
+         * android.net.ConnectivityDiagnosticsManager}.
+         * obj = ConnectivityDiagnosticsCallbackInfo with IConnectivityDiagnosticsCallback and
+         * NetworkRequestInfo to be registered
+         */
+        private static final int EVENT_REGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK = 1;
+
+        /**
+         * Used to handle ConnectivityDiagnosticsCallback unregister events from {@link
+         * android.net.ConnectivityDiagnosticsManager}.
+         * obj = the IConnectivityDiagnosticsCallback to be unregistered
+         * arg1 = the uid of the caller
+         */
+        private static final int EVENT_UNREGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK = 2;
+
+        /**
+         * Event for {@link NetworkStateTrackerHandler} to trigger ConnectivityReport callbacks
+         * after processing {@link #EVENT_NETWORK_TESTED} events.
+         * obj = {@link ConnectivityReportEvent} representing ConnectivityReport info reported from
+         * NetworkMonitor.
+         * data = PersistableBundle of extras passed from NetworkMonitor.
+         *
+         * <p>See {@link ConnectivityService#EVENT_NETWORK_TESTED}.
+         */
+        private static final int EVENT_NETWORK_TESTED = ConnectivityService.EVENT_NETWORK_TESTED;
+
+        /**
+         * Event for NetworkMonitor to inform ConnectivityService that a potential data stall has
+         * been detected on the network.
+         * obj = Long the timestamp (in millis) for when the suspected data stall was detected.
+         * arg1 = {@link DataStallReport#DetectionMethod} indicating the detection method.
+         * arg2 = NetID.
+         * data = PersistableBundle of extras passed from NetworkMonitor.
+         */
+        private static final int EVENT_DATA_STALL_SUSPECTED = 4;
+
+        /**
+         * Event for ConnectivityDiagnosticsHandler to handle network connectivity being reported to
+         * the platform. This event will invoke {@link
+         * IConnectivityDiagnosticsCallback#onNetworkConnectivityReported} for permissioned
+         * callbacks.
+         * obj = Network that was reported on
+         * arg1 = boolint for the quality reported
+         */
+        private static final int EVENT_NETWORK_CONNECTIVITY_REPORTED = 5;
+
+        private ConnectivityDiagnosticsHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case EVENT_REGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK: {
+                    handleRegisterConnectivityDiagnosticsCallback(
+                            (ConnectivityDiagnosticsCallbackInfo) msg.obj);
+                    break;
+                }
+                case EVENT_UNREGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK: {
+                    handleUnregisterConnectivityDiagnosticsCallback(
+                            (IConnectivityDiagnosticsCallback) msg.obj, msg.arg1);
+                    break;
+                }
+                case EVENT_NETWORK_TESTED: {
+                    final ConnectivityReportEvent reportEvent =
+                            (ConnectivityReportEvent) msg.obj;
+
+                    handleNetworkTestedWithExtras(reportEvent, reportEvent.mExtras);
+                    break;
+                }
+                case EVENT_DATA_STALL_SUSPECTED: {
+                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
+                    final Pair<Long, PersistableBundle> arg =
+                            (Pair<Long, PersistableBundle>) msg.obj;
+                    if (nai == null) break;
+
+                    handleDataStallSuspected(nai, arg.first, msg.arg1, arg.second);
+                    break;
+                }
+                case EVENT_NETWORK_CONNECTIVITY_REPORTED: {
+                    handleNetworkConnectivityReported((NetworkAgentInfo) msg.obj, toBool(msg.arg1));
+                    break;
+                }
+                default: {
+                    Log.e(mTag, "Unrecognized event in ConnectivityDiagnostics: " + msg.what);
+                }
+            }
+        }
+    }
+
+    /** Class used for cleaning up IConnectivityDiagnosticsCallback instances after their death. */
+    @VisibleForTesting
+    class ConnectivityDiagnosticsCallbackInfo implements Binder.DeathRecipient {
+        @NonNull private final IConnectivityDiagnosticsCallback mCb;
+        @NonNull private final NetworkRequestInfo mRequestInfo;
+        @NonNull private final String mCallingPackageName;
+
+        @VisibleForTesting
+        ConnectivityDiagnosticsCallbackInfo(
+                @NonNull IConnectivityDiagnosticsCallback cb,
+                @NonNull NetworkRequestInfo nri,
+                @NonNull String callingPackageName) {
+            mCb = cb;
+            mRequestInfo = nri;
+            mCallingPackageName = callingPackageName;
+        }
+
+        @Override
+        public void binderDied() {
+            log("ConnectivityDiagnosticsCallback IBinder died.");
+            unregisterConnectivityDiagnosticsCallback(mCb);
+        }
+    }
+
+    /**
+     * Class used for sending information from {@link
+     * NetworkMonitorCallbacks#notifyNetworkTestedWithExtras} to the handler for processing it.
+     */
+    private static class NetworkTestedResults {
+        private final int mNetId;
+        private final int mTestResult;
+        private final long mTimestampMillis;
+        @Nullable private final String mRedirectUrl;
+
+        private NetworkTestedResults(
+                int netId, int testResult, long timestampMillis, @Nullable String redirectUrl) {
+            mNetId = netId;
+            mTestResult = testResult;
+            mTimestampMillis = timestampMillis;
+            mRedirectUrl = redirectUrl;
+        }
+    }
+
+    /**
+     * Class used for sending information from {@link NetworkStateTrackerHandler} to {@link
+     * ConnectivityDiagnosticsHandler}.
+     */
+    private static class ConnectivityReportEvent {
+        private final long mTimestampMillis;
+        @NonNull private final NetworkAgentInfo mNai;
+        private final PersistableBundle mExtras;
+
+        private ConnectivityReportEvent(long timestampMillis, @NonNull NetworkAgentInfo nai,
+                PersistableBundle p) {
+            mTimestampMillis = timestampMillis;
+            mNai = nai;
+            mExtras = p;
+        }
+    }
+
+    private void handleRegisterConnectivityDiagnosticsCallback(
+            @NonNull ConnectivityDiagnosticsCallbackInfo cbInfo) {
+        ensureRunningOnConnectivityServiceThread();
+
+        final IConnectivityDiagnosticsCallback cb = cbInfo.mCb;
+        final IBinder iCb = cb.asBinder();
+        final NetworkRequestInfo nri = cbInfo.mRequestInfo;
+
+        // Connectivity Diagnostics are meant to be used with a single network request. It would be
+        // confusing for these networks to change when an NRI is satisfied in another layer.
+        if (nri.isMultilayerRequest()) {
+            throw new IllegalArgumentException("Connectivity Diagnostics do not support multilayer "
+                + "network requests.");
+        }
+
+        // This means that the client registered the same callback multiple times. Do
+        // not override the previous entry, and exit silently.
+        if (mConnectivityDiagnosticsCallbacks.containsKey(iCb)) {
+            if (VDBG) log("Diagnostics callback is already registered");
+
+            // Decrement the reference count for this NetworkRequestInfo. The reference count is
+            // incremented when the NetworkRequestInfo is created as part of
+            // enforceRequestCountLimit().
+            nri.decrementRequestCount();
+            return;
+        }
+
+        mConnectivityDiagnosticsCallbacks.put(iCb, cbInfo);
+
+        try {
+            iCb.linkToDeath(cbInfo, 0);
+        } catch (RemoteException e) {
+            cbInfo.binderDied();
+            return;
+        }
+
+        // Once registered, provide ConnectivityReports for matching Networks
+        final List<NetworkAgentInfo> matchingNetworks = new ArrayList<>();
+        synchronized (mNetworkForNetId) {
+            for (int i = 0; i < mNetworkForNetId.size(); i++) {
+                final NetworkAgentInfo nai = mNetworkForNetId.valueAt(i);
+                // Connectivity Diagnostics rejects multilayer requests at registration hence get(0)
+                if (nai.satisfies(nri.mRequests.get(0))) {
+                    matchingNetworks.add(nai);
+                }
+            }
+        }
+        for (final NetworkAgentInfo nai : matchingNetworks) {
+            final ConnectivityReport report = nai.getConnectivityReport();
+            if (report == null) {
+                continue;
+            }
+            if (!checkConnectivityDiagnosticsPermissions(
+                    nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) {
+                continue;
+            }
+
+            try {
+                cb.onConnectivityReportAvailable(report);
+            } catch (RemoteException e) {
+                // Exception while sending the ConnectivityReport. Move on to the next network.
+            }
+        }
+    }
+
+    private void handleUnregisterConnectivityDiagnosticsCallback(
+            @NonNull IConnectivityDiagnosticsCallback cb, int uid) {
+        ensureRunningOnConnectivityServiceThread();
+        final IBinder iCb = cb.asBinder();
+
+        final ConnectivityDiagnosticsCallbackInfo cbInfo =
+                mConnectivityDiagnosticsCallbacks.remove(iCb);
+        if (cbInfo == null) {
+            if (VDBG) log("Removing diagnostics callback that is not currently registered");
+            return;
+        }
+
+        final NetworkRequestInfo nri = cbInfo.mRequestInfo;
+
+        // Caller's UID must either be the registrants (if they are unregistering) or the System's
+        // (if the Binder died)
+        if (uid != nri.mUid && uid != Process.SYSTEM_UID) {
+            if (DBG) loge("Uid(" + uid + ") not registrant's (" + nri.mUid + ") or System's");
+            return;
+        }
+
+        // Decrement the reference count for this NetworkRequestInfo. The reference count is
+        // incremented when the NetworkRequestInfo is created as part of
+        // enforceRequestCountLimit().
+        nri.decrementRequestCount();
+
+        iCb.unlinkToDeath(cbInfo, 0);
+    }
+
+    private void handleNetworkTestedWithExtras(
+            @NonNull ConnectivityReportEvent reportEvent, @NonNull PersistableBundle extras) {
+        final NetworkAgentInfo nai = reportEvent.mNai;
+        final NetworkCapabilities networkCapabilities =
+                getNetworkCapabilitiesWithoutUids(nai.networkCapabilities);
+        final ConnectivityReport report =
+                new ConnectivityReport(
+                        reportEvent.mNai.network,
+                        reportEvent.mTimestampMillis,
+                        nai.linkProperties,
+                        networkCapabilities,
+                        extras);
+        nai.setConnectivityReport(report);
+        final List<IConnectivityDiagnosticsCallback> results =
+                getMatchingPermissionedCallbacks(nai);
+        for (final IConnectivityDiagnosticsCallback cb : results) {
+            try {
+                cb.onConnectivityReportAvailable(report);
+            } catch (RemoteException ex) {
+                loge("Error invoking onConnectivityReport", ex);
+            }
+        }
+    }
+
+    private void handleDataStallSuspected(
+            @NonNull NetworkAgentInfo nai, long timestampMillis, int detectionMethod,
+            @NonNull PersistableBundle extras) {
+        final NetworkCapabilities networkCapabilities =
+                getNetworkCapabilitiesWithoutUids(nai.networkCapabilities);
+        final DataStallReport report =
+                new DataStallReport(
+                        nai.network,
+                        timestampMillis,
+                        detectionMethod,
+                        nai.linkProperties,
+                        networkCapabilities,
+                        extras);
+        final List<IConnectivityDiagnosticsCallback> results =
+                getMatchingPermissionedCallbacks(nai);
+        for (final IConnectivityDiagnosticsCallback cb : results) {
+            try {
+                cb.onDataStallSuspected(report);
+            } catch (RemoteException ex) {
+                loge("Error invoking onDataStallSuspected", ex);
+            }
+        }
+    }
+
+    private void handleNetworkConnectivityReported(
+            @NonNull NetworkAgentInfo nai, boolean connectivity) {
+        final List<IConnectivityDiagnosticsCallback> results =
+                getMatchingPermissionedCallbacks(nai);
+        for (final IConnectivityDiagnosticsCallback cb : results) {
+            try {
+                cb.onNetworkConnectivityReported(nai.network, connectivity);
+            } catch (RemoteException ex) {
+                loge("Error invoking onNetworkConnectivityReported", ex);
+            }
+        }
+    }
+
+    private NetworkCapabilities getNetworkCapabilitiesWithoutUids(@NonNull NetworkCapabilities nc) {
+        final NetworkCapabilities sanitized = new NetworkCapabilities(nc,
+                NetworkCapabilities.REDACT_ALL);
+        sanitized.setUids(null);
+        sanitized.setAdministratorUids(new int[0]);
+        sanitized.setOwnerUid(Process.INVALID_UID);
+        return sanitized;
+    }
+
+    private List<IConnectivityDiagnosticsCallback> getMatchingPermissionedCallbacks(
+            @NonNull NetworkAgentInfo nai) {
+        final List<IConnectivityDiagnosticsCallback> results = new ArrayList<>();
+        for (Entry<IBinder, ConnectivityDiagnosticsCallbackInfo> entry :
+                mConnectivityDiagnosticsCallbacks.entrySet()) {
+            final ConnectivityDiagnosticsCallbackInfo cbInfo = entry.getValue();
+            final NetworkRequestInfo nri = cbInfo.mRequestInfo;
+            // Connectivity Diagnostics rejects multilayer requests at registration hence get(0).
+            if (nai.satisfies(nri.mRequests.get(0))) {
+                if (checkConnectivityDiagnosticsPermissions(
+                        nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) {
+                    results.add(entry.getValue().mCb);
+                }
+            }
+        }
+        return results;
+    }
+
+    private boolean isLocationPermissionRequiredForConnectivityDiagnostics(
+            @NonNull NetworkAgentInfo nai) {
+        // TODO(b/188483916): replace with a transport-agnostic location-aware check
+        return nai.networkCapabilities.hasTransport(TRANSPORT_WIFI);
+    }
+
+    private boolean hasLocationPermission(String packageName, int uid) {
+        // LocationPermissionChecker#checkLocationPermission can throw SecurityException if the uid
+        // and package name don't match. Throwing on the CS thread is not acceptable, so wrap the
+        // call in a try-catch.
+        try {
+            if (!mLocationPermissionChecker.checkLocationPermission(
+                        packageName, null /* featureId */, uid, null /* message */)) {
+                return false;
+            }
+        } catch (SecurityException e) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private boolean ownsVpnRunningOverNetwork(int uid, Network network) {
+        for (NetworkAgentInfo virtual : mNetworkAgentInfos) {
+            if (virtual.supportsUnderlyingNetworks()
+                    && virtual.networkCapabilities.getOwnerUid() == uid
+                    && CollectionUtils.contains(virtual.declaredUnderlyingNetworks, network)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @VisibleForTesting
+    boolean checkConnectivityDiagnosticsPermissions(
+            int callbackPid, int callbackUid, NetworkAgentInfo nai, String callbackPackageName) {
+        if (checkNetworkStackPermission(callbackPid, callbackUid)) {
+            return true;
+        }
+
+        // Administrator UIDs also contains the Owner UID
+        final int[] administratorUids = nai.networkCapabilities.getAdministratorUids();
+        if (!CollectionUtils.contains(administratorUids, callbackUid)
+                && !ownsVpnRunningOverNetwork(callbackUid, nai.network)) {
+            return false;
+        }
+
+        return !isLocationPermissionRequiredForConnectivityDiagnostics(nai)
+                || hasLocationPermission(callbackPackageName, callbackUid);
+    }
+
+    @Override
+    public void registerConnectivityDiagnosticsCallback(
+            @NonNull IConnectivityDiagnosticsCallback callback,
+            @NonNull NetworkRequest request,
+            @NonNull String callingPackageName) {
+        if (request.legacyType != TYPE_NONE) {
+            throw new IllegalArgumentException("ConnectivityManager.TYPE_* are deprecated."
+                    + " Please use NetworkCapabilities instead.");
+        }
+        final int callingUid = mDeps.getCallingUid();
+        mAppOpsManager.checkPackage(callingUid, callingPackageName);
+
+        // This NetworkCapabilities is only used for matching to Networks. Clear out its owner uid
+        // and administrator uids to be safe.
+        final NetworkCapabilities nc = new NetworkCapabilities(request.networkCapabilities);
+        restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+
+        final NetworkRequest requestWithId =
+                new NetworkRequest(
+                        nc, TYPE_NONE, nextNetworkRequestId(), NetworkRequest.Type.LISTEN);
+
+        // NetworkRequestInfos created here count towards MAX_NETWORK_REQUESTS_PER_UID limit.
+        //
+        // nri is not bound to the death of callback. Instead, callback.bindToDeath() is set in
+        // handleRegisterConnectivityDiagnosticsCallback(). nri will be cleaned up as part of the
+        // callback's binder death.
+        final NetworkRequestInfo nri = new NetworkRequestInfo(callingUid, requestWithId);
+        final ConnectivityDiagnosticsCallbackInfo cbInfo =
+                new ConnectivityDiagnosticsCallbackInfo(callback, nri, callingPackageName);
+
+        mConnectivityDiagnosticsHandler.sendMessage(
+                mConnectivityDiagnosticsHandler.obtainMessage(
+                        ConnectivityDiagnosticsHandler
+                                .EVENT_REGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK,
+                        cbInfo));
+    }
+
+    @Override
+    public void unregisterConnectivityDiagnosticsCallback(
+            @NonNull IConnectivityDiagnosticsCallback callback) {
+        Objects.requireNonNull(callback, "callback must be non-null");
+        mConnectivityDiagnosticsHandler.sendMessage(
+                mConnectivityDiagnosticsHandler.obtainMessage(
+                        ConnectivityDiagnosticsHandler
+                                .EVENT_UNREGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK,
+                        mDeps.getCallingUid(),
+                        0,
+                        callback));
+    }
+
+    @Override
+    public void simulateDataStall(int detectionMethod, long timestampMillis,
+            @NonNull Network network, @NonNull PersistableBundle extras) {
+        enforceAnyPermissionOf(android.Manifest.permission.MANAGE_TEST_NETWORKS,
+                android.Manifest.permission.NETWORK_STACK);
+        final NetworkCapabilities nc = getNetworkCapabilitiesInternal(network);
+        if (!nc.hasTransport(TRANSPORT_TEST)) {
+            throw new SecurityException("Data Stall simluation is only possible for test networks");
+        }
+
+        final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+        if (nai == null || nai.creatorUid != mDeps.getCallingUid()) {
+            throw new SecurityException("Data Stall simulation is only possible for network "
+                + "creators");
+        }
+
+        // Instead of passing the data stall directly to the ConnectivityDiagnostics handler, treat
+        // this as a Data Stall received directly from NetworkMonitor. This requires wrapping the
+        // Data Stall information as a DataStallReportParcelable and passing to
+        // #notifyDataStallSuspected. This ensures that unknown Data Stall detection methods are
+        // still passed to ConnectivityDiagnostics (with new detection methods masked).
+        final DataStallReportParcelable p = new DataStallReportParcelable();
+        p.timestampMillis = timestampMillis;
+        p.detectionMethod = detectionMethod;
+
+        if (hasDataStallDetectionMethod(p, DETECTION_METHOD_DNS_EVENTS)) {
+            p.dnsConsecutiveTimeouts = extras.getInt(KEY_DNS_CONSECUTIVE_TIMEOUTS);
+        }
+        if (hasDataStallDetectionMethod(p, DETECTION_METHOD_TCP_METRICS)) {
+            p.tcpPacketFailRate = extras.getInt(KEY_TCP_PACKET_FAIL_RATE);
+            p.tcpMetricsCollectionPeriodMillis = extras.getInt(
+                    KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS);
+        }
+
+        notifyDataStallSuspected(p, network.getNetId());
+    }
+
+    private class NetdCallback extends BaseNetdUnsolicitedEventListener {
+        @Override
+        public void onInterfaceClassActivityChanged(boolean isActive, int transportType,
+                long timestampNs, int uid) {
+            mNetworkActivityTracker.setAndReportNetworkActive(isActive, transportType, timestampNs);
+        }
+
+        @Override
+        public void onInterfaceLinkStateChanged(String iface, boolean up) {
+            for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+                nai.clatd.interfaceLinkStateChanged(iface, up);
+            }
+        }
+
+        @Override
+        public void onInterfaceRemoved(String iface) {
+            for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+                nai.clatd.interfaceRemoved(iface);
+            }
+        }
+    }
+
+    private final LegacyNetworkActivityTracker mNetworkActivityTracker;
+
+    /**
+     * Class used for updating network activity tracking with netd and notify network activity
+     * changes.
+     */
+    private static final class LegacyNetworkActivityTracker {
+        private static final int NO_UID = -1;
+        private final Context mContext;
+        private final INetd mNetd;
+        private final RemoteCallbackList<INetworkActivityListener> mNetworkActivityListeners =
+                new RemoteCallbackList<>();
+        // Indicate the current system default network activity is active or not.
+        @GuardedBy("mActiveIdleTimers")
+        private boolean mNetworkActive;
+        @GuardedBy("mActiveIdleTimers")
+        private final ArrayMap<String, IdleTimerParams> mActiveIdleTimers = new ArrayMap();
+        private final Handler mHandler;
+
+        private class IdleTimerParams {
+            public final int timeout;
+            public final int transportType;
+
+            IdleTimerParams(int timeout, int transport) {
+                this.timeout = timeout;
+                this.transportType = transport;
+            }
+        }
+
+        LegacyNetworkActivityTracker(@NonNull Context context, @NonNull Handler handler,
+                @NonNull INetd netd) {
+            mContext = context;
+            mNetd = netd;
+            mHandler = handler;
+        }
+
+        public void setAndReportNetworkActive(boolean active, int transportType, long tsNanos) {
+            sendDataActivityBroadcast(transportTypeToLegacyType(transportType), active, tsNanos);
+            synchronized (mActiveIdleTimers) {
+                mNetworkActive = active;
+                // If there are no idle timers, it means that system is not monitoring
+                // activity, so the system default network for those default network
+                // unspecified apps is always considered active.
+                //
+                // TODO: If the mActiveIdleTimers is empty, netd will actually not send
+                // any network activity change event. Whenever this event is received,
+                // the mActiveIdleTimers should be always not empty. The legacy behavior
+                // is no-op. Remove to refer to mNetworkActive only.
+                if (mNetworkActive || mActiveIdleTimers.isEmpty()) {
+                    mHandler.sendMessage(mHandler.obtainMessage(EVENT_REPORT_NETWORK_ACTIVITY));
+                }
+            }
+        }
+
+        // The network activity should only be updated from ConnectivityService handler thread
+        // when mActiveIdleTimers lock is held.
+        @GuardedBy("mActiveIdleTimers")
+        private void reportNetworkActive() {
+            final int length = mNetworkActivityListeners.beginBroadcast();
+            if (DDBG) log("reportNetworkActive, notify " + length + " listeners");
+            try {
+                for (int i = 0; i < length; i++) {
+                    try {
+                        mNetworkActivityListeners.getBroadcastItem(i).onNetworkActive();
+                    } catch (RemoteException | RuntimeException e) {
+                        loge("Fail to send network activie to listener " + e);
+                    }
+                }
+            } finally {
+                mNetworkActivityListeners.finishBroadcast();
+            }
+        }
+
+        @GuardedBy("mActiveIdleTimers")
+        public void handleReportNetworkActivity() {
+            synchronized (mActiveIdleTimers) {
+                reportNetworkActive();
+            }
+        }
+
+        // This is deprecated and only to support legacy use cases.
+        private int transportTypeToLegacyType(int type) {
+            switch (type) {
+                case NetworkCapabilities.TRANSPORT_CELLULAR:
+                    return TYPE_MOBILE;
+                case NetworkCapabilities.TRANSPORT_WIFI:
+                    return TYPE_WIFI;
+                case NetworkCapabilities.TRANSPORT_BLUETOOTH:
+                    return TYPE_BLUETOOTH;
+                case NetworkCapabilities.TRANSPORT_ETHERNET:
+                    return TYPE_ETHERNET;
+                default:
+                    loge("Unexpected transport in transportTypeToLegacyType: " + type);
+            }
+            return ConnectivityManager.TYPE_NONE;
+        }
+
+        public void sendDataActivityBroadcast(int deviceType, boolean active, long tsNanos) {
+            final Intent intent = new Intent(ConnectivityManager.ACTION_DATA_ACTIVITY_CHANGE);
+            intent.putExtra(ConnectivityManager.EXTRA_DEVICE_TYPE, deviceType);
+            intent.putExtra(ConnectivityManager.EXTRA_IS_ACTIVE, active);
+            intent.putExtra(ConnectivityManager.EXTRA_REALTIME_NS, tsNanos);
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL,
+                        RECEIVE_DATA_ACTIVITY_CHANGE,
+                        null /* resultReceiver */,
+                        null /* scheduler */,
+                        0 /* initialCode */,
+                        null /* initialData */,
+                        null /* initialExtra */);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
+        /**
+         * Setup data activity tracking for the given network.
+         *
+         * Every {@code setupDataActivityTracking} should be paired with a
+         * {@link #removeDataActivityTracking} for cleanup.
+         */
+        private void setupDataActivityTracking(NetworkAgentInfo networkAgent) {
+            final String iface = networkAgent.linkProperties.getInterfaceName();
+
+            final int timeout;
+            final int type;
+
+            if (networkAgent.networkCapabilities.hasTransport(
+                    NetworkCapabilities.TRANSPORT_CELLULAR)) {
+                timeout = Settings.Global.getInt(mContext.getContentResolver(),
+                        ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_MOBILE,
+                        10);
+                type = NetworkCapabilities.TRANSPORT_CELLULAR;
+            } else if (networkAgent.networkCapabilities.hasTransport(
+                    NetworkCapabilities.TRANSPORT_WIFI)) {
+                timeout = Settings.Global.getInt(mContext.getContentResolver(),
+                        ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_WIFI,
+                        15);
+                type = NetworkCapabilities.TRANSPORT_WIFI;
+            } else {
+                return; // do not track any other networks
+            }
+
+            updateRadioPowerState(true /* isActive */, type);
+
+            if (timeout > 0 && iface != null) {
+                try {
+                    synchronized (mActiveIdleTimers) {
+                        // Networks start up.
+                        mNetworkActive = true;
+                        mActiveIdleTimers.put(iface, new IdleTimerParams(timeout, type));
+                        mNetd.idletimerAddInterface(iface, timeout, Integer.toString(type));
+                        reportNetworkActive();
+                    }
+                } catch (Exception e) {
+                    // You shall not crash!
+                    loge("Exception in setupDataActivityTracking " + e);
+                }
+            }
+        }
+
+        /**
+         * Remove data activity tracking when network disconnects.
+         */
+        private void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+            final String iface = networkAgent.linkProperties.getInterfaceName();
+            final NetworkCapabilities caps = networkAgent.networkCapabilities;
+
+            if (iface == null) return;
+
+            final int type;
+            if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+                type = NetworkCapabilities.TRANSPORT_CELLULAR;
+            } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+                type = NetworkCapabilities.TRANSPORT_WIFI;
+            } else {
+                return; // do not track any other networks
+            }
+
+            try {
+                updateRadioPowerState(false /* isActive */, type);
+                synchronized (mActiveIdleTimers) {
+                    final IdleTimerParams params = mActiveIdleTimers.remove(iface);
+                    // The call fails silently if no idle timer setup for this interface
+                    mNetd.idletimerRemoveInterface(iface, params.timeout,
+                            Integer.toString(params.transportType));
+                }
+            } catch (Exception e) {
+                // You shall not crash!
+                loge("Exception in removeDataActivityTracking " + e);
+            }
+        }
+
+        /**
+         * Update data activity tracking when network state is updated.
+         */
+        public void updateDataActivityTracking(NetworkAgentInfo newNetwork,
+                NetworkAgentInfo oldNetwork) {
+            if (newNetwork != null) {
+                setupDataActivityTracking(newNetwork);
+            }
+            if (oldNetwork != null) {
+                removeDataActivityTracking(oldNetwork);
+            }
+        }
+
+        private void updateRadioPowerState(boolean isActive, int transportType) {
+            final BatteryStatsManager bs = mContext.getSystemService(BatteryStatsManager.class);
+            switch (transportType) {
+                case NetworkCapabilities.TRANSPORT_CELLULAR:
+                    bs.reportMobileRadioPowerState(isActive, NO_UID);
+                    break;
+                case NetworkCapabilities.TRANSPORT_WIFI:
+                    bs.reportWifiRadioPowerState(isActive, NO_UID);
+                    break;
+                default:
+                    logw("Untracked transport type:" + transportType);
+            }
+        }
+
+        public boolean isDefaultNetworkActive() {
+            synchronized (mActiveIdleTimers) {
+                // If there are no idle timers, it means that system is not monitoring activity,
+                // so the default network is always considered active.
+                //
+                // TODO : Distinguish between the cases where mActiveIdleTimers is empty because
+                // tracking is disabled (negative idle timer value configured), or no active default
+                // network. In the latter case, this reports active but it should report inactive.
+                return mNetworkActive || mActiveIdleTimers.isEmpty();
+            }
+        }
+
+        public void registerNetworkActivityListener(@NonNull INetworkActivityListener l) {
+            mNetworkActivityListeners.register(l);
+        }
+
+        public void unregisterNetworkActivityListener(@NonNull INetworkActivityListener l) {
+            mNetworkActivityListeners.unregister(l);
+        }
+
+        public void dump(IndentingPrintWriter pw) {
+            synchronized (mActiveIdleTimers) {
+                pw.print("mNetworkActive="); pw.println(mNetworkActive);
+                pw.println("Idle timers:");
+                for (HashMap.Entry<String, IdleTimerParams> ent : mActiveIdleTimers.entrySet()) {
+                    pw.print("  "); pw.print(ent.getKey()); pw.println(":");
+                    final IdleTimerParams params = ent.getValue();
+                    pw.print("    timeout="); pw.print(params.timeout);
+                    pw.print(" type="); pw.println(params.transportType);
+                }
+            }
+        }
+    }
+
+    /**
+     * Registers {@link QosSocketFilter} with {@link IQosCallback}.
+     *
+     * @param socketInfo the socket information
+     * @param callback the callback to register
+     */
+    @Override
+    public void registerQosSocketCallback(@NonNull final QosSocketInfo socketInfo,
+            @NonNull final IQosCallback callback) {
+        final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(socketInfo.getNetwork());
+        if (nai == null || nai.networkCapabilities == null) {
+            try {
+                callback.onError(QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED);
+            } catch (final RemoteException ex) {
+                loge("registerQosCallbackInternal: RemoteException", ex);
+            }
+            return;
+        }
+        registerQosCallbackInternal(new QosSocketFilter(socketInfo), callback, nai);
+    }
+
+    /**
+     * Register a {@link IQosCallback} with base {@link QosFilter}.
+     *
+     * @param filter the filter to register
+     * @param callback the callback to register
+     * @param nai the agent information related to the filter's network
+     */
+    @VisibleForTesting
+    public void registerQosCallbackInternal(@NonNull final QosFilter filter,
+            @NonNull final IQosCallback callback, @NonNull final NetworkAgentInfo nai) {
+        if (filter == null) throw new IllegalArgumentException("filter must be non-null");
+        if (callback == null) throw new IllegalArgumentException("callback must be non-null");
+
+        if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+            enforceConnectivityRestrictedNetworksPermission();
+        }
+        mQosCallbackTracker.registerCallback(callback, filter, nai);
+    }
+
+    /**
+     * Unregisters the given callback.
+     *
+     * @param callback the callback to unregister
+     */
+    @Override
+    public void unregisterQosCallback(@NonNull final IQosCallback callback) {
+        Objects.requireNonNull(callback, "callback must be non-null");
+        mQosCallbackTracker.unregisterCallback(callback);
+    }
+
+    // Network preference per-profile and OEM network preferences can't be set at the same
+    // time, because it is unclear what should happen if both preferences are active for
+    // one given UID. To make it possible, the stack would have to clarify what would happen
+    // in case both are active at the same time. The implementation may have to be adjusted
+    // to implement the resulting rules. For example, a priority could be defined between them,
+    // where the OEM preference would be considered less or more important than the enterprise
+    // preference ; this would entail implementing the priorities somehow, e.g. by doing
+    // UID arithmetic with UID ranges or passing a priority to netd so that the routing rules
+    // are set at the right level. Other solutions are possible, e.g. merging of the
+    // preferences for the relevant UIDs.
+    private static void throwConcurrentPreferenceException() {
+        throw new IllegalStateException("Can't set NetworkPreferenceForUser and "
+                + "set OemNetworkPreference at the same time");
+    }
+
+    /**
+     * Request that a user profile is put by default on a network matching a given preference.
+     *
+     * See the documentation for the individual preferences for a description of the supported
+     * behaviors.
+     *
+     * @param profile the profile concerned.
+     * @param preference the preference for this profile, as one of the PROFILE_NETWORK_PREFERENCE_*
+     *                   constants.
+     * @param listener an optional listener to listen for completion of the operation.
+     */
+    @Override
+    public void setProfileNetworkPreference(@NonNull final UserHandle profile,
+            @ConnectivityManager.ProfileNetworkPreference final int preference,
+            @Nullable final IOnCompleteListener listener) {
+        Objects.requireNonNull(profile);
+        PermissionUtils.enforceNetworkStackPermission(mContext);
+        if (DBG) {
+            log("setProfileNetworkPreference " + profile + " to " + preference);
+        }
+        if (profile.getIdentifier() < 0) {
+            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");
+        }
+        // Strictly speaking, mOemNetworkPreferences should only be touched on the
+        // handler thread. However it is an immutable object, so reading the reference is
+        // safe - it's just possible the value is slightly outdated. For the final check,
+        // see #handleSetProfileNetworkPreference. But if this can be caught here it is a
+        // lot easier to understand, so opportunistically check it.
+        // TODO: Have a priority for each preference.
+        if (!mOemNetworkPreferences.isEmpty() || !mMobileDataPreferredUids.isEmpty()) {
+            throwConcurrentPreferenceException();
+        }
+        final NetworkCapabilities nc;
+        switch (preference) {
+            case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT:
+                nc = null;
+                break;
+            case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE:
+                final UidRange uids = UidRange.createForUser(profile);
+                nc = createDefaultNetworkCapabilitiesForUidRange(uids);
+                nc.addCapability(NET_CAPABILITY_ENTERPRISE);
+                nc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Invalid preference in setProfileNetworkPreference");
+        }
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_PROFILE_NETWORK_PREFERENCE,
+                new Pair<>(new ProfileNetworkPreferences.Preference(profile, nc), listener)));
+    }
+
+    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 ProfileNetworkPreferences prefs) {
+        final ArraySet<NetworkRequestInfo> result = new ArraySet<>();
+        for (final ProfileNetworkPreferences.Preference pref : prefs.preferences) {
+            // The NRI for a user should be comprised of two layers:
+            // - The request for the capabilities
+            // - The request for the default network, for fallback. Create an image of it to
+            //   have the correct UIDs in it (also a request can only be part of one NRI, because
+            //   of lookups in 1:1 associations like mNetworkRequests).
+            // Note that denying a fallback can be implemented simply by not adding the second
+            // request.
+            final ArrayList<NetworkRequest> nrs = new ArrayList<>();
+            nrs.add(createNetworkRequest(NetworkRequest.Type.REQUEST, pref.capabilities));
+            nrs.add(createDefaultInternetRequestForTransport(
+                    TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
+            setNetworkRequestUids(nrs, UidRange.fromIntRanges(pref.capabilities.getUids()));
+            final NetworkRequestInfo nri = new NetworkRequestInfo(Process.myUid(), nrs,
+                    DEFAULT_NETWORK_PRIORITY_PROFILE);
+            result.add(nri);
+        }
+        return result;
+    }
+
+    private void handleSetProfileNetworkPreference(
+            @NonNull final ProfileNetworkPreferences.Preference preference,
+            @Nullable final IOnCompleteListener listener) {
+        // setProfileNetworkPreference and setOemNetworkPreference are mutually exclusive, in
+        // particular because it's not clear what preference should win in case both apply
+        // to the same app.
+        // The binder call has already checked this, but as mOemNetworkPreferences is only
+        // touched on the handler thread, it's theoretically not impossible that it has changed
+        // since.
+        // TODO: Have a priority for each preference.
+        if (!mOemNetworkPreferences.isEmpty() || !mMobileDataPreferredUids.isEmpty()) {
+            // This may happen on a device with an OEM preference set when a user is removed.
+            // In this case, it's safe to ignore. In particular this happens in the tests.
+            loge("handleSetProfileNetworkPreference, but OEM network preferences not empty");
+            return;
+        }
+
+        validateNetworkCapabilitiesOfProfileNetworkPreference(preference.capabilities);
+
+        mProfileNetworkPreferences = mProfileNetworkPreferences.plus(preference);
+        mSystemNetworkRequestCounter.transact(
+                mDeps.getCallingUid(), mProfileNetworkPreferences.preferences.size(),
+                () -> {
+                    final ArraySet<NetworkRequestInfo> nris =
+                            createNrisFromProfileNetworkPreferences(mProfileNetworkPreferences);
+                    replaceDefaultNetworkRequestsForPreference(nris);
+                });
+        // Finally, rematch.
+        rematchAllNetworksAndRequests();
+
+        if (null != listener) {
+            try {
+                listener.onComplete();
+            } catch (RemoteException e) {
+                loge("Listener for setProfileNetworkPreference has died");
+            }
+        }
+    }
+
+    @VisibleForTesting
+    @NonNull
+    ArraySet<NetworkRequestInfo> createNrisFromMobileDataPreferredUids(
+            @NonNull final Set<Integer> uids) {
+        final ArraySet<NetworkRequestInfo> nris = new ArraySet<>();
+        if (uids.size() == 0) {
+            // Should not create NetworkRequestInfo if no preferences. Without uid range in
+            // NetworkRequestInfo, makeDefaultForApps() would treat it as a illegal NRI.
+            if (DBG) log("Don't create NetworkRequestInfo because no preferences");
+            return nris;
+        }
+
+        final List<NetworkRequest> requests = new ArrayList<>();
+        // The NRI should be comprised of two layers:
+        // - The request for the mobile network preferred.
+        // - The request for the default network, for fallback.
+        requests.add(createDefaultInternetRequestForTransport(
+                TRANSPORT_CELLULAR, NetworkRequest.Type.LISTEN));
+        requests.add(createDefaultInternetRequestForTransport(
+                TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
+        final Set<UidRange> ranges = new ArraySet<>();
+        for (final int uid : uids) {
+            ranges.add(new UidRange(uid, uid));
+        }
+        setNetworkRequestUids(requests, ranges);
+        nris.add(new NetworkRequestInfo(Process.myUid(), requests,
+                DEFAULT_NETWORK_PRIORITY_MOBILE_DATA_PREFERRED));
+        return nris;
+    }
+
+    private void handleMobileDataPreferredUidsChanged() {
+        // Ignore update preference because it's not clear what preference should win in case both
+        // apply to the same app.
+        // TODO: Have a priority for each preference.
+        if (!mOemNetworkPreferences.isEmpty() || !mProfileNetworkPreferences.isEmpty()) {
+            loge("Ignore mobile data preference change because other preferences are not empty");
+            return;
+        }
+
+        mMobileDataPreferredUids = ConnectivitySettingsManager.getMobileDataPreferredUids(mContext);
+        mSystemNetworkRequestCounter.transact(
+                mDeps.getCallingUid(), 1 /* numOfNewRequests */,
+                () -> {
+                    final ArraySet<NetworkRequestInfo> nris =
+                            createNrisFromMobileDataPreferredUids(mMobileDataPreferredUids);
+                    replaceDefaultNetworkRequestsForPreference(nris);
+                });
+        // Finally, rematch.
+        rematchAllNetworksAndRequests();
+    }
+
+    private void enforceAutomotiveDevice() {
+        final boolean isAutomotiveDevice =
+                mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+        if (!isAutomotiveDevice) {
+            throw new UnsupportedOperationException(
+                    "setOemNetworkPreference() is only available on automotive devices.");
+        }
+    }
+
+    /**
+     * Used by automotive devices to set the network preferences used to direct traffic at an
+     * application level as per the given OemNetworkPreferences. An example use-case would be an
+     * automotive OEM wanting to provide connectivity for applications critical to the usage of a
+     * vehicle via a particular network.
+     *
+     * Calling this will overwrite the existing preference.
+     *
+     * @param preference {@link OemNetworkPreferences} The application network preference to be set.
+     * @param listener {@link ConnectivityManager.OnCompleteListener} Listener used
+     * to communicate completion of setOemNetworkPreference();
+     */
+    @Override
+    public void setOemNetworkPreference(
+            @NonNull final OemNetworkPreferences preference,
+            @Nullable final IOnCompleteListener listener) {
+
+        Objects.requireNonNull(preference, "OemNetworkPreferences must be non-null");
+        // Only bypass the permission/device checks if this is a valid test request.
+        if (isValidTestOemNetworkPreference(preference)) {
+            enforceManageTestNetworksPermission();
+        } else {
+            enforceAutomotiveDevice();
+            enforceOemNetworkPreferencesPermission();
+            validateOemNetworkPreferences(preference);
+        }
+
+        // TODO: Have a priority for each preference.
+        if (!mProfileNetworkPreferences.isEmpty() || !mMobileDataPreferredUids.isEmpty()) {
+            // Strictly speaking, mProfileNetworkPreferences should only be touched on the
+            // handler thread. However it is an immutable object, so reading the reference is
+            // safe - it's just possible the value is slightly outdated. For the final check,
+            // see #handleSetOemPreference. But if this can be caught here it is a
+            // lot easier to understand, so opportunistically check it.
+            throwConcurrentPreferenceException();
+        }
+
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_OEM_NETWORK_PREFERENCE,
+                new Pair<>(preference, listener)));
+    }
+
+    /**
+     * Check the validity of an OEM network preference to be used for testing purposes.
+     * @param preference the preference to validate
+     * @return true if this is a valid OEM network preference test request.
+     */
+    private boolean isValidTestOemNetworkPreference(
+            @NonNull final OemNetworkPreferences preference) {
+        // Allow for clearing of an existing OemNetworkPreference used for testing.
+        // This isn't called on the handler thread so it is possible that mOemNetworkPreferences
+        // changes after this check is complete. This is an unlikely scenario as calling of this API
+        // is controlled by the OEM therefore the added complexity is not worth adding given those
+        // circumstances. That said, it is an edge case to be aware of hence this comment.
+        final boolean isValidTestClearPref = preference.getNetworkPreferences().size() == 0
+                && isTestOemNetworkPreference(mOemNetworkPreferences);
+        return isTestOemNetworkPreference(preference) || isValidTestClearPref;
+    }
+
+    private boolean isTestOemNetworkPreference(@NonNull final OemNetworkPreferences preference) {
+        final Map<String, Integer> prefMap = preference.getNetworkPreferences();
+        return prefMap.size() == 1
+                && (prefMap.containsValue(OEM_NETWORK_PREFERENCE_TEST)
+                || prefMap.containsValue(OEM_NETWORK_PREFERENCE_TEST_ONLY));
+    }
+
+    private void validateOemNetworkPreferences(@NonNull OemNetworkPreferences preference) {
+        for (@OemNetworkPreferences.OemNetworkPreference final int pref
+                : preference.getNetworkPreferences().values()) {
+            if (pref <= 0 || OemNetworkPreferences.OEM_NETWORK_PREFERENCE_MAX < pref) {
+                throw new IllegalArgumentException(
+                        OemNetworkPreferences.oemNetworkPreferenceToString(pref)
+                                + " is an invalid value.");
+            }
+        }
+    }
+
+    private void handleSetOemNetworkPreference(
+            @NonNull final OemNetworkPreferences preference,
+            @Nullable final IOnCompleteListener listener) {
+        Objects.requireNonNull(preference, "OemNetworkPreferences must be non-null");
+        if (DBG) {
+            log("set OEM network preferences :" + preference.toString());
+        }
+        // setProfileNetworkPreference and setOemNetworkPreference are mutually exclusive, in
+        // particular because it's not clear what preference should win in case both apply
+        // to the same app.
+        // The binder call has already checked this, but as mOemNetworkPreferences is only
+        // touched on the handler thread, it's theoretically not impossible that it has changed
+        // since.
+        // TODO: Have a priority for each preference.
+        if (!mProfileNetworkPreferences.isEmpty() || !mMobileDataPreferredUids.isEmpty()) {
+            logwtf("handleSetOemPreference, but per-profile network preferences not empty");
+            return;
+        }
+
+        mOemNetworkPreferencesLogs.log("UPDATE INITIATED: " + preference);
+        final int uniquePreferenceCount = new ArraySet<>(
+                preference.getNetworkPreferences().values()).size();
+        mSystemNetworkRequestCounter.transact(
+                mDeps.getCallingUid(), uniquePreferenceCount,
+                () -> {
+                    final ArraySet<NetworkRequestInfo> nris =
+                            new OemNetworkRequestFactory()
+                                    .createNrisFromOemNetworkPreferences(preference);
+                    replaceDefaultNetworkRequestsForPreference(nris);
+                });
+        mOemNetworkPreferences = preference;
+
+        if (null != listener) {
+            try {
+                listener.onComplete();
+            } catch (RemoteException e) {
+                loge("Can't send onComplete in handleSetOemNetworkPreference", e);
+            }
+        }
+    }
+
+    private void replaceDefaultNetworkRequestsForPreference(
+            @NonNull final Set<NetworkRequestInfo> nris) {
+        // Pass in a defensive copy as this collection will be updated on remove.
+        handleRemoveNetworkRequests(new ArraySet<>(mDefaultNetworkRequests));
+        addPerAppDefaultNetworkRequests(nris);
+    }
+
+    private void addPerAppDefaultNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) {
+        ensureRunningOnConnectivityServiceThread();
+        mDefaultNetworkRequests.addAll(nris);
+        final ArraySet<NetworkRequestInfo> perAppCallbackRequestsToUpdate =
+                getPerAppCallbackRequestsToUpdate();
+        final ArraySet<NetworkRequestInfo> nrisToRegister = new ArraySet<>(nris);
+        mSystemNetworkRequestCounter.transact(
+                mDeps.getCallingUid(), perAppCallbackRequestsToUpdate.size(),
+                () -> {
+                    nrisToRegister.addAll(
+                            createPerAppCallbackRequestsToRegister(perAppCallbackRequestsToUpdate));
+                    handleRemoveNetworkRequests(perAppCallbackRequestsToUpdate);
+                    handleRegisterNetworkRequests(nrisToRegister);
+                });
+    }
+
+    /**
+     * All current requests that are tracking the default network need to be assessed as to whether
+     * or not the current set of per-application default requests will be changing their default
+     * network. If so, those requests will need to be updated so that they will send callbacks for
+     * default network changes at the appropriate time. Additionally, those requests tracking the
+     * default that were previously updated by this flow will need to be reassessed.
+     * @return the nris which will need to be updated.
+     */
+    private ArraySet<NetworkRequestInfo> getPerAppCallbackRequestsToUpdate() {
+        final ArraySet<NetworkRequestInfo> defaultCallbackRequests = new ArraySet<>();
+        // Get the distinct nris to check since for multilayer requests, it is possible to have the
+        // same nri in the map's values for each of its NetworkRequest objects.
+        final ArraySet<NetworkRequestInfo> nris = new ArraySet<>(mNetworkRequests.values());
+        for (final NetworkRequestInfo nri : nris) {
+            // Include this nri if it is currently being tracked.
+            if (isPerAppTrackedNri(nri)) {
+                defaultCallbackRequests.add(nri);
+                continue;
+            }
+            // We only track callbacks for requests tracking the default.
+            if (NetworkRequest.Type.TRACK_DEFAULT != nri.mRequests.get(0).type) {
+                continue;
+            }
+            // Include this nri if it will be tracked by the new per-app default requests.
+            final boolean isNriGoingToBeTracked =
+                    getDefaultRequestTrackingUid(nri.mAsUid) != mDefaultRequest;
+            if (isNriGoingToBeTracked) {
+                defaultCallbackRequests.add(nri);
+            }
+        }
+        return defaultCallbackRequests;
+    }
+
+    /**
+     * Create nris for those network requests that are currently tracking the default network that
+     * are being controlled by a per-application default.
+     * @param perAppCallbackRequestsForUpdate the baseline network requests to be used as the
+     * foundation when creating the nri. Important items include the calling uid's original
+     * NetworkRequest to be used when mapping callbacks as well as the caller's uid and name. These
+     * requests are assumed to have already been validated as needing to be updated.
+     * @return the Set of nris to use when registering network requests.
+     */
+    private ArraySet<NetworkRequestInfo> createPerAppCallbackRequestsToRegister(
+            @NonNull final ArraySet<NetworkRequestInfo> perAppCallbackRequestsForUpdate) {
+        final ArraySet<NetworkRequestInfo> callbackRequestsToRegister = new ArraySet<>();
+        for (final NetworkRequestInfo callbackRequest : perAppCallbackRequestsForUpdate) {
+            final NetworkRequestInfo trackingNri =
+                    getDefaultRequestTrackingUid(callbackRequest.mAsUid);
+
+            // If this nri is not being tracked, the change it back to an untracked nri.
+            if (trackingNri == mDefaultRequest) {
+                callbackRequestsToRegister.add(new NetworkRequestInfo(
+                        callbackRequest,
+                        Collections.singletonList(callbackRequest.getNetworkRequestForCallback())));
+                continue;
+            }
+
+            final NetworkRequest request = callbackRequest.mRequests.get(0);
+            callbackRequestsToRegister.add(new NetworkRequestInfo(
+                    callbackRequest,
+                    copyNetworkRequestsForUid(
+                            trackingNri.mRequests, callbackRequest.mAsUid,
+                            callbackRequest.mUid, request.getRequestorPackageName())));
+        }
+        return callbackRequestsToRegister;
+    }
+
+    private static void setNetworkRequestUids(@NonNull final List<NetworkRequest> requests,
+            @NonNull final Set<UidRange> uids) {
+        for (final NetworkRequest req : requests) {
+            req.networkCapabilities.setUids(UidRange.toIntRanges(uids));
+        }
+    }
+
+    /**
+     * Class used to generate {@link NetworkRequestInfo} based off of {@link OemNetworkPreferences}.
+     */
+    @VisibleForTesting
+    final class OemNetworkRequestFactory {
+        ArraySet<NetworkRequestInfo> createNrisFromOemNetworkPreferences(
+                @NonNull final OemNetworkPreferences preference) {
+            final ArraySet<NetworkRequestInfo> nris = new ArraySet<>();
+            final SparseArray<Set<Integer>> uids =
+                    createUidsFromOemNetworkPreferences(preference);
+            for (int i = 0; i < uids.size(); i++) {
+                final int key = uids.keyAt(i);
+                final Set<Integer> value = uids.valueAt(i);
+                final NetworkRequestInfo nri = createNriFromOemNetworkPreferences(key, value);
+                // No need to add an nri without any requests.
+                if (0 == nri.mRequests.size()) {
+                    continue;
+                }
+                nris.add(nri);
+            }
+
+            return nris;
+        }
+
+        private SparseArray<Set<Integer>> createUidsFromOemNetworkPreferences(
+                @NonNull final OemNetworkPreferences preference) {
+            final SparseArray<Set<Integer>> prefToUids = new SparseArray<>();
+            final PackageManager pm = mContext.getPackageManager();
+            final List<UserHandle> users =
+                    mContext.getSystemService(UserManager.class).getUserHandles(true);
+            if (null == users || users.size() == 0) {
+                if (VDBG || DDBG) {
+                    log("No users currently available for setting the OEM network preference.");
+                }
+                return prefToUids;
+            }
+            for (final Map.Entry<String, Integer> entry :
+                    preference.getNetworkPreferences().entrySet()) {
+                @OemNetworkPreferences.OemNetworkPreference final int pref = entry.getValue();
+                // Add the rules for all users as this policy is device wide.
+                for (final UserHandle user : users) {
+                    try {
+                        final int uid = pm.getApplicationInfoAsUser(entry.getKey(), 0, user).uid;
+                        if (!prefToUids.contains(pref)) {
+                            prefToUids.put(pref, new ArraySet<>());
+                        }
+                        prefToUids.get(pref).add(uid);
+                    } catch (PackageManager.NameNotFoundException e) {
+                        // Although this may seem like an error scenario, it is ok that uninstalled
+                        // packages are sent on a network preference as the system will watch for
+                        // package installations associated with this network preference and update
+                        // accordingly. This is done to minimize race conditions on app install.
+                        continue;
+                    }
+                }
+            }
+            return prefToUids;
+        }
+
+        private NetworkRequestInfo createNriFromOemNetworkPreferences(
+                @OemNetworkPreferences.OemNetworkPreference final int preference,
+                @NonNull final Set<Integer> uids) {
+            final List<NetworkRequest> requests = new ArrayList<>();
+            // Requests will ultimately be evaluated by order of insertion therefore it matters.
+            switch (preference) {
+                case OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID:
+                    requests.add(createUnmeteredNetworkRequest());
+                    requests.add(createOemPaidNetworkRequest());
+                    requests.add(createDefaultInternetRequestForTransport(
+                            TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
+                    break;
+                case OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK:
+                    requests.add(createUnmeteredNetworkRequest());
+                    requests.add(createOemPaidNetworkRequest());
+                    break;
+                case OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY:
+                    requests.add(createOemPaidNetworkRequest());
+                    break;
+                case OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY:
+                    requests.add(createOemPrivateNetworkRequest());
+                    break;
+                case OEM_NETWORK_PREFERENCE_TEST:
+                    requests.add(createUnmeteredNetworkRequest());
+                    requests.add(createTestNetworkRequest());
+                    requests.add(createDefaultRequest());
+                    break;
+                case OEM_NETWORK_PREFERENCE_TEST_ONLY:
+                    requests.add(createTestNetworkRequest());
+                    break;
+                default:
+                    // This should never happen.
+                    throw new IllegalArgumentException("createNriFromOemNetworkPreferences()"
+                            + " called with invalid preference of " + preference);
+            }
+
+            final ArraySet<UidRange> ranges = new ArraySet<>();
+            for (final int uid : uids) {
+                ranges.add(new UidRange(uid, uid));
+            }
+            setNetworkRequestUids(requests, ranges);
+            return new NetworkRequestInfo(
+                    Process.myUid(), requests, DEFAULT_NETWORK_PRIORITY_OEM);
+        }
+
+        private NetworkRequest createUnmeteredNetworkRequest() {
+            final NetworkCapabilities netcap = createDefaultPerAppNetCap()
+                    .addCapability(NET_CAPABILITY_NOT_METERED)
+                    .addCapability(NET_CAPABILITY_VALIDATED);
+            return createNetworkRequest(NetworkRequest.Type.LISTEN, netcap);
+        }
+
+        private NetworkRequest createOemPaidNetworkRequest() {
+            // NET_CAPABILITY_OEM_PAID is a restricted capability.
+            final NetworkCapabilities netcap = createDefaultPerAppNetCap()
+                    .addCapability(NET_CAPABILITY_OEM_PAID)
+                    .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+            return createNetworkRequest(NetworkRequest.Type.REQUEST, netcap);
+        }
+
+        private NetworkRequest createOemPrivateNetworkRequest() {
+            // NET_CAPABILITY_OEM_PRIVATE is a restricted capability.
+            final NetworkCapabilities netcap = createDefaultPerAppNetCap()
+                    .addCapability(NET_CAPABILITY_OEM_PRIVATE)
+                    .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+            return createNetworkRequest(NetworkRequest.Type.REQUEST, netcap);
+        }
+
+        private NetworkCapabilities createDefaultPerAppNetCap() {
+            final NetworkCapabilities netcap = new NetworkCapabilities();
+            netcap.addCapability(NET_CAPABILITY_INTERNET);
+            netcap.setRequestorUidAndPackageName(Process.myUid(), mContext.getPackageName());
+            return netcap;
+        }
+
+        private NetworkRequest createTestNetworkRequest() {
+            final NetworkCapabilities netcap = new NetworkCapabilities();
+            netcap.clearAll();
+            netcap.addTransportType(TRANSPORT_TEST);
+            return createNetworkRequest(NetworkRequest.Type.REQUEST, netcap);
+        }
+    }
+}
diff --git a/service/src/com/android/server/ConnectivityServiceInitializer.java b/service/src/com/android/server/ConnectivityServiceInitializer.java
new file mode 100644
index 0000000..2465479
--- /dev/null
+++ b/service/src/com/android/server/ConnectivityServiceInitializer.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.util.Log;
+
+/**
+ * Connectivity service initializer for core networking. This is called by system server to create
+ * a new instance of ConnectivityService.
+ */
+public final class ConnectivityServiceInitializer extends SystemService {
+    private static final String TAG = ConnectivityServiceInitializer.class.getSimpleName();
+    private final ConnectivityService mConnectivity;
+
+    public ConnectivityServiceInitializer(Context context) {
+        super(context);
+        // Load JNI libraries used by ConnectivityService and its dependencies
+        System.loadLibrary("service-connectivity");
+        // TODO: Define formal APIs to get the needed services.
+        mConnectivity = new ConnectivityService(context);
+    }
+
+    @Override
+    public void onStart() {
+        Log.i(TAG, "Registering " + Context.CONNECTIVITY_SERVICE);
+        publishBinderService(Context.CONNECTIVITY_SERVICE, mConnectivity,
+                /* allowIsolated= */ false);
+    }
+}
diff --git a/service/src/com/android/server/NetIdManager.java b/service/src/com/android/server/NetIdManager.java
new file mode 100644
index 0000000..61925c8
--- /dev/null
+++ b/service/src/com/android/server/NetIdManager.java
@@ -0,0 +1,89 @@
+/*
+ * 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 com.android.server;
+
+import android.annotation.NonNull;
+import android.net.ConnectivityManager;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Class used to reserve and release net IDs.
+ *
+ * <p>Instances of this class are thread-safe.
+ */
+public class NetIdManager {
+    // Sequence number for Networks; keep in sync with system/netd/NetworkController.cpp
+    public static final int MIN_NET_ID = 100; // some reserved marks
+    // Top IDs reserved by IpSecService
+    public static final int MAX_NET_ID = ConnectivityManager.getIpSecNetIdRange().getLower() - 1;
+
+    @GuardedBy("mNetIdInUse")
+    private final SparseBooleanArray mNetIdInUse = new SparseBooleanArray();
+
+    @GuardedBy("mNetIdInUse")
+    private int mLastNetId = MIN_NET_ID - 1;
+
+    private final int mMaxNetId;
+
+    public NetIdManager() {
+        this(MAX_NET_ID);
+    }
+
+    @VisibleForTesting
+    NetIdManager(int maxNetId) {
+        mMaxNetId = maxNetId;
+    }
+
+    /**
+     * Get the first netId that follows the provided lastId and is available.
+     */
+    private int getNextAvailableNetIdLocked(
+            int lastId, @NonNull SparseBooleanArray netIdInUse) {
+        int netId = lastId;
+        for (int i = MIN_NET_ID; i <= mMaxNetId; i++) {
+            netId = netId < mMaxNetId ? netId + 1 : MIN_NET_ID;
+            if (!netIdInUse.get(netId)) {
+                return netId;
+            }
+        }
+        throw new IllegalStateException("No free netIds");
+    }
+
+    /**
+     * Reserve a new ID for a network.
+     */
+    public int reserveNetId() {
+        synchronized (mNetIdInUse) {
+            mLastNetId = getNextAvailableNetIdLocked(mLastNetId, mNetIdInUse);
+            // Make sure NetID unused.  http://b/16815182
+            mNetIdInUse.put(mLastNetId, true);
+            return mLastNetId;
+        }
+    }
+
+    /**
+     * Clear a previously reserved ID for a network.
+     */
+    public void releaseNetId(int id) {
+        synchronized (mNetIdInUse) {
+            mNetIdInUse.delete(id);
+        }
+    }
+}
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
new file mode 100644
index 0000000..fffd2be
--- /dev/null
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -0,0 +1,380 @@
+/*
+ * 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;
+
+import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
+import static android.net.TestNetworkManager.TEST_TUN_PREFIX;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.ITestNetworkManager;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.RouteInfo;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkSpecifier;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.NetworkStackConstants;
+
+import java.io.UncheckedIOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** @hide */
+class TestNetworkService extends ITestNetworkManager.Stub {
+    @NonNull private static final String TEST_NETWORK_LOGTAG = "TestNetworkAgent";
+    @NonNull private static final String TEST_NETWORK_PROVIDER_NAME = "TestNetworkProvider";
+    @NonNull private static final AtomicInteger sTestTunIndex = new AtomicInteger();
+
+    @NonNull private final Context mContext;
+    @NonNull private final INetd mNetd;
+
+    @NonNull private final HandlerThread mHandlerThread;
+    @NonNull private final Handler mHandler;
+
+    @NonNull private final ConnectivityManager mCm;
+    @NonNull private final NetworkProvider mNetworkProvider;
+
+    // Native method stubs
+    private static native int jniCreateTunTap(boolean isTun, @NonNull String iface);
+
+    @VisibleForTesting
+    protected TestNetworkService(@NonNull Context context) {
+        mHandlerThread = new HandlerThread("TestNetworkServiceThread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+
+        mContext = Objects.requireNonNull(context, "missing Context");
+        mNetd = Objects.requireNonNull(
+                INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
+                "could not get netd instance");
+        mCm = mContext.getSystemService(ConnectivityManager.class);
+        mNetworkProvider = new NetworkProvider(mContext, mHandler.getLooper(),
+                TEST_NETWORK_PROVIDER_NAME);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mCm.registerNetworkProvider(mNetworkProvider);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Create a TUN or TAP interface with the given interface name and link addresses
+     *
+     * <p>This method will return the FileDescriptor to the interface. Close it to tear down the
+     * interface.
+     */
+    private TestNetworkInterface createInterface(boolean isTun, LinkAddress[] linkAddrs) {
+        enforceTestNetworkPermissions(mContext);
+
+        Objects.requireNonNull(linkAddrs, "missing linkAddrs");
+
+        String ifacePrefix = isTun ? TEST_TUN_PREFIX : TEST_TAP_PREFIX;
+        String iface = ifacePrefix + sTestTunIndex.getAndIncrement();
+        final long token = Binder.clearCallingIdentity();
+        try {
+            ParcelFileDescriptor tunIntf =
+                    ParcelFileDescriptor.adoptFd(jniCreateTunTap(isTun, iface));
+            for (LinkAddress addr : linkAddrs) {
+                mNetd.interfaceAddAddress(
+                        iface,
+                        addr.getAddress().getHostAddress(),
+                        addr.getPrefixLength());
+            }
+
+            NetdUtils.setInterfaceUp(mNetd, iface);
+
+            return new TestNetworkInterface(tunIntf, iface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Create a TUN interface with the given interface name and link addresses
+     *
+     * <p>This method will return the FileDescriptor to the TUN interface. Close it to tear down the
+     * TUN interface.
+     */
+    @Override
+    public TestNetworkInterface createTunInterface(@NonNull LinkAddress[] linkAddrs) {
+        return createInterface(true, linkAddrs);
+    }
+
+    /**
+     * Create a TAP interface with the given interface name
+     *
+     * <p>This method will return the FileDescriptor to the TAP interface. Close it to tear down the
+     * TAP interface.
+     */
+    @Override
+    public TestNetworkInterface createTapInterface() {
+        return createInterface(false, new LinkAddress[0]);
+    }
+
+    // Tracker for TestNetworkAgents
+    @GuardedBy("mTestNetworkTracker")
+    @NonNull
+    private final SparseArray<TestNetworkAgent> mTestNetworkTracker = new SparseArray<>();
+
+    public class TestNetworkAgent extends NetworkAgent implements IBinder.DeathRecipient {
+        private static final int NETWORK_SCORE = 1; // Use a low, non-zero score.
+
+        private final int mUid;
+
+        @GuardedBy("mBinderLock")
+        @NonNull
+        private IBinder mBinder;
+
+        @NonNull private final Object mBinderLock = new Object();
+
+        private TestNetworkAgent(
+                @NonNull Context context,
+                @NonNull Looper looper,
+                @NonNull NetworkCapabilities nc,
+                @NonNull LinkProperties lp,
+                @NonNull NetworkAgentConfig config,
+                int uid,
+                @NonNull IBinder binder,
+                @NonNull NetworkProvider np)
+                throws RemoteException {
+            super(context, looper, TEST_NETWORK_LOGTAG, nc, lp, NETWORK_SCORE, config, np);
+            mUid = uid;
+            synchronized (mBinderLock) {
+                mBinder = binder; // Binder null-checks in create()
+
+                try {
+                    mBinder.linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    binderDied();
+                    throw e; // Abort, signal failure up the stack.
+                }
+            }
+        }
+
+        /**
+         * If the Binder object dies, this function is called to free the resources of this
+         * TestNetworkAgent
+         */
+        @Override
+        public void binderDied() {
+            teardown();
+        }
+
+        @Override
+        protected void unwanted() {
+            teardown();
+        }
+
+        private void teardown() {
+            unregister();
+
+            // Synchronize on mBinderLock to ensure that unlinkToDeath is never called more than
+            // once (otherwise it could throw an exception)
+            synchronized (mBinderLock) {
+                // If mBinder is null, this Test Network has already been cleaned up.
+                if (mBinder == null) return;
+                mBinder.unlinkToDeath(this, 0);
+                mBinder = null;
+            }
+
+            // Has to be in TestNetworkAgent to ensure all teardown codepaths properly clean up
+            // resources, even for binder death or unwanted calls.
+            synchronized (mTestNetworkTracker) {
+                mTestNetworkTracker.remove(getNetwork().getNetId());
+            }
+        }
+    }
+
+    private TestNetworkAgent registerTestNetworkAgent(
+            @NonNull Looper looper,
+            @NonNull Context context,
+            @NonNull String iface,
+            @Nullable LinkProperties lp,
+            boolean isMetered,
+            int callingUid,
+            @NonNull int[] administratorUids,
+            @NonNull IBinder binder)
+            throws RemoteException, SocketException {
+        Objects.requireNonNull(looper, "missing Looper");
+        Objects.requireNonNull(context, "missing Context");
+        // iface and binder validity checked by caller
+
+        // Build narrow set of NetworkCapabilities, useful only for testing
+        NetworkCapabilities nc = new NetworkCapabilities();
+        nc.clearAll(); // Remove default capabilities.
+        nc.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+        nc.setNetworkSpecifier(new TestNetworkSpecifier(iface));
+        nc.setAdministratorUids(administratorUids);
+        if (!isMetered) {
+            nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+        }
+
+        // Build LinkProperties
+        if (lp == null) {
+            lp = new LinkProperties();
+        } else {
+            lp = new LinkProperties(lp);
+            // Use LinkAddress(es) from the interface itself to minimize how much the caller
+            // is trusted.
+            lp.setLinkAddresses(new ArrayList<>());
+        }
+        lp.setInterfaceName(iface);
+
+        // Find the currently assigned addresses, and add them to LinkProperties
+        boolean allowIPv4 = false, allowIPv6 = false;
+        NetworkInterface netIntf = NetworkInterface.getByName(iface);
+        Objects.requireNonNull(netIntf, "No such network interface found: " + netIntf);
+
+        for (InterfaceAddress intfAddr : netIntf.getInterfaceAddresses()) {
+            lp.addLinkAddress(
+                    new LinkAddress(intfAddr.getAddress(), intfAddr.getNetworkPrefixLength()));
+
+            if (intfAddr.getAddress() instanceof Inet6Address) {
+                allowIPv6 |= !intfAddr.getAddress().isLinkLocalAddress();
+            } else if (intfAddr.getAddress() instanceof Inet4Address) {
+                allowIPv4 = true;
+            }
+        }
+
+        // Add global routes (but as non-default, non-internet providing network)
+        if (allowIPv4) {
+            lp.addRoute(new RouteInfo(new IpPrefix(
+                    NetworkStackConstants.IPV4_ADDR_ANY, 0), null, iface));
+        }
+        if (allowIPv6) {
+            lp.addRoute(new RouteInfo(new IpPrefix(
+                    NetworkStackConstants.IPV6_ADDR_ANY, 0), null, iface));
+        }
+
+        final TestNetworkAgent agent = new TestNetworkAgent(context, looper, nc, lp,
+                new NetworkAgentConfig.Builder().build(), callingUid, binder,
+                mNetworkProvider);
+        agent.register();
+        agent.markConnected();
+        return agent;
+    }
+
+    /**
+     * Sets up a Network with extremely limited privileges, guarded by the MANAGE_TEST_NETWORKS
+     * permission.
+     *
+     * <p>This method provides a Network that is useful only for testing.
+     */
+    @Override
+    public void setupTestNetwork(
+            @NonNull String iface,
+            @Nullable LinkProperties lp,
+            boolean isMetered,
+            @NonNull int[] administratorUids,
+            @NonNull IBinder binder) {
+        enforceTestNetworkPermissions(mContext);
+
+        Objects.requireNonNull(iface, "missing Iface");
+        Objects.requireNonNull(binder, "missing IBinder");
+
+        if (!(iface.startsWith(INetd.IPSEC_INTERFACE_PREFIX)
+                || iface.startsWith(TEST_TUN_PREFIX))) {
+            throw new IllegalArgumentException(
+                    "Cannot create network for non ipsec, non-testtun interface");
+        }
+
+        try {
+            // Synchronize all accesses to mTestNetworkTracker to prevent the case where:
+            // 1. TestNetworkAgent successfully binds to death of binder
+            // 2. Before it is added to the mTestNetworkTracker, binder dies, binderDied() is called
+            // (on a different thread)
+            // 3. This thread is pre-empted, put() is called after remove()
+            synchronized (mTestNetworkTracker) {
+                TestNetworkAgent agent =
+                        registerTestNetworkAgent(
+                                mHandler.getLooper(),
+                                mContext,
+                                iface,
+                                lp,
+                                isMetered,
+                                Binder.getCallingUid(),
+                                administratorUids,
+                                binder);
+
+                mTestNetworkTracker.put(agent.getNetwork().getNetId(), agent);
+            }
+        } catch (SocketException e) {
+            throw new UncheckedIOException(e);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** Teardown a test network */
+    @Override
+    public void teardownTestNetwork(int netId) {
+        enforceTestNetworkPermissions(mContext);
+
+        final TestNetworkAgent agent;
+        synchronized (mTestNetworkTracker) {
+            agent = mTestNetworkTracker.get(netId);
+        }
+
+        if (agent == null) {
+            return; // Already torn down
+        } else if (agent.mUid != Binder.getCallingUid()) {
+            throw new SecurityException("Attempted to modify other user's test networks");
+        }
+
+        // Safe to be called multiple times.
+        agent.teardown();
+    }
+
+    private static final String PERMISSION_NAME =
+            android.Manifest.permission.MANAGE_TEST_NETWORKS;
+
+    public static void enforceTestNetworkPermissions(@NonNull Context context) {
+        context.enforceCallingOrSelfPermission(PERMISSION_NAME, "TestNetworkService");
+    }
+}
diff --git a/service/src/com/android/server/connectivity/AutodestructReference.java b/service/src/com/android/server/connectivity/AutodestructReference.java
new file mode 100644
index 0000000..009a43e
--- /dev/null
+++ b/service/src/com/android/server/connectivity/AutodestructReference.java
@@ -0,0 +1,42 @@
+/*
+ * 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 com.android.server.connectivity;
+
+import android.annotation.NonNull;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A ref that autodestructs at the first usage of it.
+ * @param <T> The type of the held object
+ * @hide
+ */
+public class AutodestructReference<T> {
+    private final AtomicReference<T> mHeld;
+    public AutodestructReference(@NonNull T obj) {
+        if (null == obj) throw new NullPointerException("Autodestruct reference to null");
+        mHeld = new AtomicReference<>(obj);
+    }
+
+    /** Get the ref and destruct it. NPE if already destructed. */
+    @NonNull
+    public T getAndDestroy() {
+        final T obj = mHeld.getAndSet(null);
+        if (null == obj) throw new NullPointerException("Already autodestructed");
+        return obj;
+    }
+}
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
new file mode 100644
index 0000000..05b12ba
--- /dev/null
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -0,0 +1,494 @@
+/*
+ * 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.connectivity;
+
+import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_MAX_SAMPLES;
+import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_MIN_SAMPLES;
+import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS;
+import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_DEFAULT_MODE;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_SPECIFIER;
+import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE;
+import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS;
+
+import android.annotation.NonNull;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.ConnectivitySettingsManager;
+import android.net.IDnsResolver;
+import android.net.InetAddresses;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.ResolverOptionsParcel;
+import android.net.ResolverParamsParcel;
+import android.net.Uri;
+import android.net.shared.PrivateDnsConfig;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * Encapsulate the management of DNS settings for networks.
+ *
+ * This class it NOT designed for concurrent access. Furthermore, all non-static
+ * methods MUST be called from ConnectivityService's thread. However, an exceptional
+ * case is getPrivateDnsConfig(Network) which is exclusively for
+ * ConnectivityService#dumpNetworkDiagnostics() on a random binder thread.
+ *
+ * [ Private DNS ]
+ * The code handling Private DNS is spread across several components, but this
+ * seems like the least bad place to collect all the observations.
+ *
+ * Private DNS handling and updating occurs in response to several different
+ * events. Each is described here with its corresponding intended handling.
+ *
+ * [A] Event: A new network comes up.
+ * Mechanics:
+ *     [1] ConnectivityService gets notifications from NetworkAgents.
+ *     [2] in updateNetworkInfo(), the first time the NetworkAgent goes into
+ *         into CONNECTED state, the Private DNS configuration is retrieved,
+ *         programmed, and strict mode hostname resolution (if applicable) is
+ *         enqueued in NetworkAgent's NetworkMonitor, via a call to
+ *         handlePerNetworkPrivateDnsConfig().
+ *     [3] Re-resolution of strict mode hostnames that fail to return any
+ *         IP addresses happens inside NetworkMonitor; it sends itself a
+ *         delayed CMD_EVALUATE_PRIVATE_DNS message in a simple backoff
+ *         schedule.
+ *     [4] Successfully resolved hostnames are sent to ConnectivityService
+ *         inside an EVENT_PRIVATE_DNS_CONFIG_RESOLVED message. The resolved
+ *         IP addresses are programmed into netd via:
+ *
+ *             updatePrivateDns() -> updateDnses()
+ *
+ *         both of which make calls into DnsManager.
+ *     [5] Upon a successful hostname resolution NetworkMonitor initiates a
+ *         validation attempt in the form of a lookup for a one-time hostname
+ *         that uses Private DNS.
+ *
+ * [B] Event: Private DNS settings are changed.
+ * Mechanics:
+ *     [1] ConnectivityService gets notifications from its SettingsObserver.
+ *     [2] handlePrivateDnsSettingsChanged() is called, which calls
+ *         handlePerNetworkPrivateDnsConfig() and the process proceeds
+ *         as if from A.3 above.
+ *
+ * [C] Event: An application calls ConnectivityManager#reportBadNetwork().
+ * Mechanics:
+ *     [1] NetworkMonitor is notified and initiates a reevaluation, which
+ *         always bypasses Private DNS.
+ *     [2] Once completed, NetworkMonitor checks if strict mode is in operation
+ *         and if so enqueues another evaluation of Private DNS, as if from
+ *         step A.5 above.
+ *
+ * @hide
+ */
+public class DnsManager {
+    private static final String TAG = DnsManager.class.getSimpleName();
+    private static final PrivateDnsConfig PRIVATE_DNS_OFF = new PrivateDnsConfig();
+
+    /* Defaults for resolver parameters. */
+    private static final int DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS = 1800;
+    private static final int DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT = 25;
+    private static final int DNS_RESOLVER_DEFAULT_MIN_SAMPLES = 8;
+    private static final int DNS_RESOLVER_DEFAULT_MAX_SAMPLES = 64;
+
+    /**
+     * Get PrivateDnsConfig.
+     */
+    public static PrivateDnsConfig getPrivateDnsConfig(Context context) {
+        final int mode = ConnectivitySettingsManager.getPrivateDnsMode(context);
+
+        final boolean useTls = mode != PRIVATE_DNS_MODE_OFF;
+
+        if (PRIVATE_DNS_MODE_PROVIDER_HOSTNAME == mode) {
+            final String specifier = getStringSetting(context.getContentResolver(),
+                    PRIVATE_DNS_SPECIFIER);
+            return new PrivateDnsConfig(specifier, null);
+        }
+
+        return new PrivateDnsConfig(useTls);
+    }
+
+    public static Uri[] getPrivateDnsSettingsUris() {
+        return new Uri[]{
+            Settings.Global.getUriFor(PRIVATE_DNS_DEFAULT_MODE),
+            Settings.Global.getUriFor(PRIVATE_DNS_MODE),
+            Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER),
+        };
+    }
+
+    public static class PrivateDnsValidationUpdate {
+        public final int netId;
+        public final InetAddress ipAddress;
+        public final String hostname;
+        // Refer to IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_*.
+        public final int validationResult;
+
+        public PrivateDnsValidationUpdate(int netId, InetAddress ipAddress,
+                String hostname, int validationResult) {
+            this.netId = netId;
+            this.ipAddress = ipAddress;
+            this.hostname = hostname;
+            this.validationResult = validationResult;
+        }
+    }
+
+    private static class PrivateDnsValidationStatuses {
+        enum ValidationStatus {
+            IN_PROGRESS,
+            FAILED,
+            SUCCEEDED
+        }
+
+        // Validation statuses of <hostname, ipAddress> pairs for a single netId
+        // Caution : not thread-safe. As mentioned in the top file comment, all
+        // methods of this class must only be called on ConnectivityService's thread.
+        private Map<Pair<String, InetAddress>, ValidationStatus> mValidationMap;
+
+        private PrivateDnsValidationStatuses() {
+            mValidationMap = new HashMap<>();
+        }
+
+        private boolean hasValidatedServer() {
+            for (ValidationStatus status : mValidationMap.values()) {
+                if (status == ValidationStatus.SUCCEEDED) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private void updateTrackedDnses(String[] ipAddresses, String hostname) {
+            Set<Pair<String, InetAddress>> latestDnses = new HashSet<>();
+            for (String ipAddress : ipAddresses) {
+                try {
+                    latestDnses.add(new Pair(hostname,
+                            InetAddresses.parseNumericAddress(ipAddress)));
+                } catch (IllegalArgumentException e) {}
+            }
+            // Remove <hostname, ipAddress> pairs that should not be tracked.
+            for (Iterator<Map.Entry<Pair<String, InetAddress>, ValidationStatus>> it =
+                    mValidationMap.entrySet().iterator(); it.hasNext(); ) {
+                Map.Entry<Pair<String, InetAddress>, ValidationStatus> entry = it.next();
+                if (!latestDnses.contains(entry.getKey())) {
+                    it.remove();
+                }
+            }
+            // Add new <hostname, ipAddress> pairs that should be tracked.
+            for (Pair<String, InetAddress> p : latestDnses) {
+                if (!mValidationMap.containsKey(p)) {
+                    mValidationMap.put(p, ValidationStatus.IN_PROGRESS);
+                }
+            }
+        }
+
+        private void updateStatus(PrivateDnsValidationUpdate update) {
+            Pair<String, InetAddress> p = new Pair(update.hostname,
+                    update.ipAddress);
+            if (!mValidationMap.containsKey(p)) {
+                return;
+            }
+            if (update.validationResult == VALIDATION_RESULT_SUCCESS) {
+                mValidationMap.put(p, ValidationStatus.SUCCEEDED);
+            } else if (update.validationResult == VALIDATION_RESULT_FAILURE) {
+                mValidationMap.put(p, ValidationStatus.FAILED);
+            } else {
+                Log.e(TAG, "Unknown private dns validation operation="
+                        + update.validationResult);
+            }
+        }
+
+        private LinkProperties fillInValidatedPrivateDns(LinkProperties lp) {
+            lp.setValidatedPrivateDnsServers(Collections.EMPTY_LIST);
+            mValidationMap.forEach((key, value) -> {
+                    if (value == ValidationStatus.SUCCEEDED) {
+                        lp.addValidatedPrivateDnsServer(key.second);
+                    }
+                });
+            return lp;
+        }
+    }
+
+    private final Context mContext;
+    private final ContentResolver mContentResolver;
+    private final IDnsResolver mDnsResolver;
+    private final ConcurrentHashMap<Integer, PrivateDnsConfig> mPrivateDnsMap;
+    // TODO: Replace the Map with SparseArrays.
+    private final Map<Integer, PrivateDnsValidationStatuses> mPrivateDnsValidationMap;
+    private final Map<Integer, LinkProperties> mLinkPropertiesMap;
+    private final Map<Integer, int[]> mTransportsMap;
+
+    private int mSampleValidity;
+    private int mSuccessThreshold;
+    private int mMinSamples;
+    private int mMaxSamples;
+
+    public DnsManager(Context ctx, IDnsResolver dnsResolver) {
+        mContext = ctx;
+        mContentResolver = mContext.getContentResolver();
+        mDnsResolver = dnsResolver;
+        mPrivateDnsMap = new ConcurrentHashMap<>();
+        mPrivateDnsValidationMap = new HashMap<>();
+        mLinkPropertiesMap = new HashMap<>();
+        mTransportsMap = new HashMap<>();
+
+        // TODO: Create and register ContentObservers to track every setting
+        // used herein, posting messages to respond to changes.
+    }
+
+    public PrivateDnsConfig getPrivateDnsConfig() {
+        return getPrivateDnsConfig(mContext);
+    }
+
+    public void removeNetwork(Network network) {
+        mPrivateDnsMap.remove(network.getNetId());
+        mPrivateDnsValidationMap.remove(network.getNetId());
+        mTransportsMap.remove(network.getNetId());
+        mLinkPropertiesMap.remove(network.getNetId());
+    }
+
+    // This is exclusively called by ConnectivityService#dumpNetworkDiagnostics() which
+    // is not on the ConnectivityService handler thread.
+    public PrivateDnsConfig getPrivateDnsConfig(@NonNull Network network) {
+        return mPrivateDnsMap.getOrDefault(network.getNetId(), PRIVATE_DNS_OFF);
+    }
+
+    public PrivateDnsConfig updatePrivateDns(Network network, PrivateDnsConfig cfg) {
+        Log.w(TAG, "updatePrivateDns(" + network + ", " + cfg + ")");
+        return (cfg != null)
+                ? mPrivateDnsMap.put(network.getNetId(), cfg)
+                : mPrivateDnsMap.remove(network.getNetId());
+    }
+
+    public void updatePrivateDnsStatus(int netId, LinkProperties lp) {
+        // Use the PrivateDnsConfig data pushed to this class instance
+        // from ConnectivityService.
+        final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
+                PRIVATE_DNS_OFF);
+
+        final boolean useTls = privateDnsCfg.useTls;
+        final PrivateDnsValidationStatuses statuses =
+                useTls ? mPrivateDnsValidationMap.get(netId) : null;
+        final boolean validated = (null != statuses) && statuses.hasValidatedServer();
+        final boolean strictMode = privateDnsCfg.inStrictMode();
+        final String tlsHostname = strictMode ? privateDnsCfg.hostname : null;
+        final boolean usingPrivateDns = strictMode || validated;
+
+        lp.setUsePrivateDns(usingPrivateDns);
+        lp.setPrivateDnsServerName(tlsHostname);
+        if (usingPrivateDns && null != statuses) {
+            statuses.fillInValidatedPrivateDns(lp);
+        } else {
+            lp.setValidatedPrivateDnsServers(Collections.EMPTY_LIST);
+        }
+    }
+
+    public void updatePrivateDnsValidation(PrivateDnsValidationUpdate update) {
+        final PrivateDnsValidationStatuses statuses = mPrivateDnsValidationMap.get(update.netId);
+        if (statuses == null) return;
+        statuses.updateStatus(update);
+    }
+
+    /**
+     * When creating a new network or transport types are changed in a specific network,
+     * transport types are always saved to a hashMap before update dns config.
+     * When destroying network, the specific network will be removed from the hashMap.
+     * The hashMap is always accessed on the same thread.
+     */
+    public void updateTransportsForNetwork(int netId, @NonNull int[] transportTypes) {
+        mTransportsMap.put(netId, transportTypes);
+        sendDnsConfigurationForNetwork(netId);
+    }
+
+    /**
+     * When {@link LinkProperties} are changed in a specific network, they are
+     * always saved to a hashMap before update dns config.
+     * When destroying network, the specific network will be removed from the hashMap.
+     * The hashMap is always accessed on the same thread.
+     */
+    public void noteDnsServersForNetwork(int netId, @NonNull LinkProperties lp) {
+        mLinkPropertiesMap.put(netId, lp);
+        sendDnsConfigurationForNetwork(netId);
+    }
+
+    /**
+     * Send dns configuration parameters to resolver for a given network.
+     */
+    public void sendDnsConfigurationForNetwork(int netId) {
+        final LinkProperties lp = mLinkPropertiesMap.get(netId);
+        final int[] transportTypes = mTransportsMap.get(netId);
+        if (lp == null || transportTypes == null) return;
+        updateParametersSettings();
+        final ResolverParamsParcel paramsParcel = new ResolverParamsParcel();
+
+        // We only use the PrivateDnsConfig data pushed to this class instance
+        // from ConnectivityService because it works in coordination with
+        // NetworkMonitor to decide which networks need validation and runs the
+        // blocking calls to resolve Private DNS strict mode hostnames.
+        //
+        // At this time we do not attempt to enable Private DNS on non-Internet
+        // networks like IMS.
+        final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
+                PRIVATE_DNS_OFF);
+        final boolean useTls = privateDnsCfg.useTls;
+        final boolean strictMode = privateDnsCfg.inStrictMode();
+
+        paramsParcel.netId = netId;
+        paramsParcel.sampleValiditySeconds = mSampleValidity;
+        paramsParcel.successThreshold = mSuccessThreshold;
+        paramsParcel.minSamples = mMinSamples;
+        paramsParcel.maxSamples = mMaxSamples;
+        paramsParcel.servers = makeStrings(lp.getDnsServers());
+        paramsParcel.domains = getDomainStrings(lp.getDomains());
+        paramsParcel.tlsName = strictMode ? privateDnsCfg.hostname : "";
+        paramsParcel.tlsServers =
+                strictMode ? makeStrings(
+                        Arrays.stream(privateDnsCfg.ips)
+                              .filter((ip) -> lp.isReachable(ip))
+                              .collect(Collectors.toList()))
+                : useTls ? paramsParcel.servers  // Opportunistic
+                : new String[0];            // Off
+        paramsParcel.resolverOptions = new ResolverOptionsParcel();
+        paramsParcel.transportTypes = transportTypes;
+        // Prepare to track the validation status of the DNS servers in the
+        // resolver config when private DNS is in opportunistic or strict mode.
+        if (useTls) {
+            if (!mPrivateDnsValidationMap.containsKey(netId)) {
+                mPrivateDnsValidationMap.put(netId, new PrivateDnsValidationStatuses());
+            }
+            mPrivateDnsValidationMap.get(netId).updateTrackedDnses(paramsParcel.tlsServers,
+                    paramsParcel.tlsName);
+        } else {
+            mPrivateDnsValidationMap.remove(netId);
+        }
+
+        Log.d(TAG, String.format("sendDnsConfigurationForNetwork(%d, %s, %s, %d, %d, %d, %d, "
+                + "%d, %d, %s, %s)", paramsParcel.netId, Arrays.toString(paramsParcel.servers),
+                Arrays.toString(paramsParcel.domains), paramsParcel.sampleValiditySeconds,
+                paramsParcel.successThreshold, paramsParcel.minSamples,
+                paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
+                paramsParcel.retryCount, paramsParcel.tlsName,
+                Arrays.toString(paramsParcel.tlsServers)));
+
+        try {
+            mDnsResolver.setResolverConfiguration(paramsParcel);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Error setting DNS configuration: " + e);
+            return;
+        }
+    }
+
+    /**
+     * Flush DNS caches and events work before boot has completed.
+     */
+    public void flushVmDnsCache() {
+        /*
+         * Tell the VMs to toss their DNS caches
+         */
+        final Intent intent = new Intent(ConnectivityManager.ACTION_CLEAR_DNS_CACHE);
+        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+        /*
+         * Connectivity events can happen before boot has completed ...
+         */
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private void updateParametersSettings() {
+        mSampleValidity = getIntSetting(
+                DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS,
+                DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS);
+        if (mSampleValidity < 0 || mSampleValidity > 65535) {
+            Log.w(TAG, "Invalid sampleValidity=" + mSampleValidity + ", using default="
+                    + DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS);
+            mSampleValidity = DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS;
+        }
+
+        mSuccessThreshold = getIntSetting(
+                DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT,
+                DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT);
+        if (mSuccessThreshold < 0 || mSuccessThreshold > 100) {
+            Log.w(TAG, "Invalid successThreshold=" + mSuccessThreshold + ", using default="
+                    + DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT);
+            mSuccessThreshold = DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT;
+        }
+
+        mMinSamples = getIntSetting(DNS_RESOLVER_MIN_SAMPLES, DNS_RESOLVER_DEFAULT_MIN_SAMPLES);
+        mMaxSamples = getIntSetting(DNS_RESOLVER_MAX_SAMPLES, DNS_RESOLVER_DEFAULT_MAX_SAMPLES);
+        if (mMinSamples < 0 || mMinSamples > mMaxSamples || mMaxSamples > 64) {
+            Log.w(TAG, "Invalid sample count (min, max)=(" + mMinSamples + ", " + mMaxSamples
+                    + "), using default=(" + DNS_RESOLVER_DEFAULT_MIN_SAMPLES + ", "
+                    + DNS_RESOLVER_DEFAULT_MAX_SAMPLES + ")");
+            mMinSamples = DNS_RESOLVER_DEFAULT_MIN_SAMPLES;
+            mMaxSamples = DNS_RESOLVER_DEFAULT_MAX_SAMPLES;
+        }
+    }
+
+    private int getIntSetting(String which, int dflt) {
+        return Settings.Global.getInt(mContentResolver, which, dflt);
+    }
+
+    /**
+     * Create a string array of host addresses from a collection of InetAddresses
+     *
+     * @param addrs a Collection of InetAddresses
+     * @return an array of Strings containing their host addresses
+     */
+    private String[] makeStrings(Collection<InetAddress> addrs) {
+        String[] result = new String[addrs.size()];
+        int i = 0;
+        for (InetAddress addr : addrs) {
+            result[i++] = addr.getHostAddress();
+        }
+        return result;
+    }
+
+    private static String getStringSetting(ContentResolver cr, String which) {
+        return Settings.Global.getString(cr, which);
+    }
+
+    private static String[] getDomainStrings(String domains) {
+        return (TextUtils.isEmpty(domains)) ? new String[0] : domains.split(" ");
+    }
+}
diff --git a/service/src/com/android/server/connectivity/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java
new file mode 100644
index 0000000..14cec09
--- /dev/null
+++ b/service/src/com/android/server/connectivity/FullScore.java
@@ -0,0 +1,343 @@
+/*
+ * 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 android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkScore.KEEP_CONNECTED_NONE;
+import static android.net.NetworkScore.POLICY_EXITING;
+import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY;
+import static android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkScore;
+import android.net.NetworkScore.KeepConnectedReason;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.StringJoiner;
+
+/**
+ * This class represents how desirable a network is.
+ *
+ * FullScore is very similar to NetworkScore, but it contains the bits that are managed
+ * by ConnectivityService. This provides static guarantee that all users must know whether
+ * they are handling a score that had the CS-managed bits set.
+ */
+public class FullScore {
+    // 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;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"POLICY_"}, value = {
+            POLICY_IS_VALIDATED,
+            POLICY_IS_VPN,
+            POLICY_EVER_USER_SELECTED,
+            POLICY_ACCEPT_UNVALIDATED,
+            POLICY_IS_UNMETERED
+    })
+    public @interface Policy {
+    }
+
+    // Agent-managed policies are in NetworkScore. They start from 1.
+    // CS-managed policies, counting from 63 downward
+    // This network is validated. CS-managed because the source of truth is in NetworkCapabilities.
+    /** @hide */
+    public static final int POLICY_IS_VALIDATED = 63;
+
+    // This is a VPN and behaves as one for scoring purposes.
+    /** @hide */
+    public static final int POLICY_IS_VPN = 62;
+
+    // This network has been selected by the user manually from settings or a 3rd party app
+    // at least once. {@see NetworkAgentConfig#explicitlySelected}.
+    /** @hide */
+    public static final int POLICY_EVER_USER_SELECTED = 61;
+
+    // The user has indicated in UI that this network should be used even if it doesn't
+    // validate. {@see NetworkAgentConfig#acceptUnvalidated}.
+    /** @hide */
+    public static final int POLICY_ACCEPT_UNVALIDATED = 60;
+
+    // This network is unmetered. {@see NetworkCapabilities.NET_CAPABILITY_NOT_METERED}.
+    /** @hide */
+    public static final int POLICY_IS_UNMETERED = 59;
+
+    // This network is invincible. This is useful for offers until there is an API to listen
+    // to requests.
+    /** @hide */
+    public static final int POLICY_IS_INVINCIBLE = 58;
+
+    // This network has been validated at least once since it was connected, but not explicitly
+    // avoided in UI.
+    // TODO : remove setAvoidUnvalidated and instead disconnect the network when the user
+    // chooses to move away from this network, and remove this flag.
+    /** @hide */
+    public static final int POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD = 57;
+
+    // To help iterate when printing
+    @VisibleForTesting
+    static final int MIN_CS_MANAGED_POLICY = POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD;
+    @VisibleForTesting
+    static final int MAX_CS_MANAGED_POLICY = POLICY_IS_VALIDATED;
+
+    // Mask for policies in NetworkScore. This should have all bits managed by NetworkScore set
+    // and all bits managed by FullScore unset. As bits are handled from 0 up in NetworkScore and
+    // from 63 down in FullScore, cut at the 32nd bit for simplicity, but change this if some day
+    // there are more than 32 bits handled on either side.
+    // YIELD_TO_BAD_WIFI is temporarily handled by ConnectivityService.
+    private static final long EXTERNAL_POLICIES_MASK =
+            0x00000000FFFFFFFFL & ~(1L << POLICY_YIELD_TO_BAD_WIFI);
+
+    @VisibleForTesting
+    static @NonNull String policyNameOf(final int policy) {
+        switch (policy) {
+            case POLICY_IS_VALIDATED: return "IS_VALIDATED";
+            case POLICY_IS_VPN: return "IS_VPN";
+            case POLICY_EVER_USER_SELECTED: return "EVER_USER_SELECTED";
+            case POLICY_ACCEPT_UNVALIDATED: return "ACCEPT_UNVALIDATED";
+            case POLICY_IS_UNMETERED: return "IS_UNMETERED";
+            case POLICY_YIELD_TO_BAD_WIFI: return "YIELD_TO_BAD_WIFI";
+            case POLICY_TRANSPORT_PRIMARY: return "TRANSPORT_PRIMARY";
+            case POLICY_EXITING: return "EXITING";
+            case POLICY_IS_INVINCIBLE: return "INVINCIBLE";
+            case POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD: return "EVER_VALIDATED";
+        }
+        throw new IllegalArgumentException("Unknown policy : " + policy);
+    }
+
+    // Bitmask of all the policies applied to this score.
+    private final long mPolicies;
+
+    private final int mKeepConnectedReason;
+
+    FullScore(final int legacyInt, final long policies,
+            @KeepConnectedReason final int keepConnectedReason) {
+        mLegacyInt = legacyInt;
+        mPolicies = policies;
+        mKeepConnectedReason = keepConnectedReason;
+    }
+
+    /**
+     * Given a score supplied by the NetworkAgent and CS-managed objects, produce a full score.
+     *
+     * @param score the score supplied by the agent
+     * @param caps the NetworkCapabilities of the network
+     * @param config the NetworkAgentConfig of the network
+     * @param everValidated whether this network has ever validated
+     * @param yieldToBadWiFi whether this network yields to a previously validated wifi gone bad
+     * @return a FullScore that is appropriate to use for ranking.
+     */
+    // TODO : this shouldn't manage bad wifi avoidance – instead this should be done by the
+    // telephony factory, so that it depends on the carrier. For now this is handled by
+    // connectivity for backward compatibility.
+    public static FullScore fromNetworkScore(@NonNull final NetworkScore score,
+            @NonNull final NetworkCapabilities caps, @NonNull final NetworkAgentConfig config,
+            final boolean everValidated, final boolean yieldToBadWiFi) {
+        return withPolicies(score.getLegacyInt(), score.getPolicies(),
+                score.getKeepConnectedReason(),
+                caps.hasCapability(NET_CAPABILITY_VALIDATED),
+                caps.hasTransport(TRANSPORT_VPN),
+                caps.hasCapability(NET_CAPABILITY_NOT_METERED),
+                everValidated,
+                config.explicitlySelected,
+                config.acceptUnvalidated,
+                yieldToBadWiFi,
+                false /* invincible */); // only prospective scores can be invincible
+    }
+
+    /**
+     * Given a score supplied by a NetworkProvider, produce a prospective score for an offer.
+     *
+     * NetworkOffers have score filters that are compared to the scores of actual networks
+     * to see if they could possibly beat the current satisfier. Some things the agent can't
+     * know in advance ; a good example is the validation bit – some networks will validate,
+     * others won't. For comparison purposes, assume the best, so all possibly beneficial
+     * networks will be brought up.
+     *
+     * @param score the score supplied by the agent for this offer
+     * @param caps the capabilities supplied by the agent for this offer
+     * @return a FullScore appropriate for comparing to actual network's scores.
+     */
+    public static FullScore makeProspectiveScore(@NonNull final NetworkScore score,
+            @NonNull final NetworkCapabilities caps) {
+        // If the network offers Internet access, it may validate.
+        final boolean mayValidate = caps.hasCapability(NET_CAPABILITY_INTERNET);
+        // VPN transports are known in advance.
+        final boolean vpn = caps.hasTransport(TRANSPORT_VPN);
+        // Prospective scores are always unmetered, because unmetered networks are stronger
+        // than metered networks, and it's not known in advance whether the network is metered.
+        final boolean unmetered = true;
+        // If the offer may validate, then it should be considered to have validated at some point
+        final boolean everValidated = mayValidate;
+        // The network hasn't been chosen by the user (yet, at least).
+        final boolean everUserSelected = false;
+        // Don't assume the user will accept unvalidated connectivity.
+        final boolean acceptUnvalidated = false;
+        // Don't assume clinging to bad wifi
+        final boolean yieldToBadWiFi = false;
+        // A prospective score is invincible if the legacy int in the filter is over the maximum
+        // score.
+        final boolean invincible = score.getLegacyInt() > NetworkRanker.LEGACY_INT_MAX;
+        return withPolicies(score.getLegacyInt(), score.getPolicies(), KEEP_CONNECTED_NONE,
+                mayValidate, vpn, unmetered, everValidated, everUserSelected, acceptUnvalidated,
+                yieldToBadWiFi, invincible);
+    }
+
+    /**
+     * Return a new score given updated caps and config.
+     *
+     * @param caps the NetworkCapabilities of the network
+     * @param config the NetworkAgentConfig of the network
+     * @return a score with the policies from the arguments reset
+     */
+    // TODO : this shouldn't manage bad wifi avoidance – instead this should be done by the
+    // telephony factory, so that it depends on the carrier. For now this is handled by
+    // connectivity for backward compatibility.
+    public FullScore mixInScore(@NonNull final NetworkCapabilities caps,
+            @NonNull final NetworkAgentConfig config,
+            final boolean everValidated,
+            final boolean yieldToBadWifi) {
+        return withPolicies(mLegacyInt, mPolicies, mKeepConnectedReason,
+                caps.hasCapability(NET_CAPABILITY_VALIDATED),
+                caps.hasTransport(TRANSPORT_VPN),
+                caps.hasCapability(NET_CAPABILITY_NOT_METERED),
+                everValidated,
+                config.explicitlySelected,
+                config.acceptUnvalidated,
+                yieldToBadWifi,
+                false /* invincible */); // only prospective scores can be invincible
+    }
+
+    // TODO : this shouldn't manage bad wifi avoidance – instead this should be done by the
+    // telephony factory, so that it depends on the carrier. For now this is handled by
+    // connectivity for backward compatibility.
+    private static FullScore withPolicies(@NonNull final int legacyInt,
+            final long externalPolicies,
+            @KeepConnectedReason final int keepConnectedReason,
+            final boolean isValidated,
+            final boolean isVpn,
+            final boolean isUnmetered,
+            final boolean everValidated,
+            final boolean everUserSelected,
+            final boolean acceptUnvalidated,
+            final boolean yieldToBadWiFi,
+            final boolean invincible) {
+        return new FullScore(legacyInt, (externalPolicies & EXTERNAL_POLICIES_MASK)
+                | (isValidated       ? 1L << POLICY_IS_VALIDATED : 0)
+                | (isVpn             ? 1L << POLICY_IS_VPN : 0)
+                | (isUnmetered       ? 1L << POLICY_IS_UNMETERED : 0)
+                | (everValidated     ? 1L << POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD : 0)
+                | (everUserSelected  ? 1L << POLICY_EVER_USER_SELECTED : 0)
+                | (acceptUnvalidated ? 1L << POLICY_ACCEPT_UNVALIDATED : 0)
+                | (yieldToBadWiFi    ? 1L << POLICY_YIELD_TO_BAD_WIFI : 0)
+                | (invincible        ? 1L << POLICY_IS_INVINCIBLE : 0),
+                keepConnectedReason);
+    }
+
+    /**
+     * Returns this score but validated.
+     */
+    public FullScore asValidated() {
+        return new FullScore(mLegacyInt, mPolicies | (1L << POLICY_IS_VALIDATED),
+                mKeepConnectedReason);
+    }
+
+    /**
+     * For backward compatibility, get the legacy int.
+     * This will be removed before S is published.
+     */
+    public int getLegacyInt() {
+        return getLegacyInt(false /* pretendValidated */);
+    }
+
+    public int getLegacyIntAsValidated() {
+        return getLegacyInt(true /* pretendValidated */);
+    }
+
+    // TODO : remove these two constants
+    // Penalty applied to scores of Networks that have not been validated.
+    private static final int UNVALIDATED_SCORE_PENALTY = 40;
+
+    // Score for a network that can be used unvalidated
+    private static final int ACCEPT_UNVALIDATED_NETWORK_SCORE = 100;
+
+    private int getLegacyInt(boolean pretendValidated) {
+        // If the user has chosen this network at least once, give it the maximum score when
+        // checking to pretend it's validated, or if it doesn't need to validate because the
+        // user said to use it even if it doesn't validate.
+        // This ensures that networks that have been selected in UI are not torn down before the
+        // user gets a chance to prefer it when a higher-scoring network (e.g., Ethernet) is
+        // available.
+        if (hasPolicy(POLICY_EVER_USER_SELECTED)
+                && (hasPolicy(POLICY_ACCEPT_UNVALIDATED) || pretendValidated)) {
+            return ACCEPT_UNVALIDATED_NETWORK_SCORE;
+        }
+
+        int score = mLegacyInt;
+        // Except for VPNs, networks are subject to a penalty for not being validated.
+        // Apply the penalty unless the network is a VPN, or it's validated or pretending to be.
+        if (!hasPolicy(POLICY_IS_VALIDATED) && !pretendValidated && !hasPolicy(POLICY_IS_VPN)) {
+            score -= UNVALIDATED_SCORE_PENALTY;
+        }
+        if (score < 0) score = 0;
+        return score;
+    }
+
+    /**
+     * @return whether this score has a particular policy.
+     */
+    @VisibleForTesting
+    public boolean hasPolicy(final int policy) {
+        return 0 != (mPolicies & (1L << policy));
+    }
+
+    /**
+     * Returns the keep-connected reason, or KEEP_CONNECTED_NONE.
+     */
+    public int getKeepConnectedReason() {
+        return mKeepConnectedReason;
+    }
+
+    // Example output :
+    // Score(50 ; Policies : EVER_USER_SELECTED&IS_VALIDATED)
+    @Override
+    public String toString() {
+        final StringJoiner sj = new StringJoiner(
+                "&", // delimiter
+                "Score(" + mLegacyInt + " ; KeepConnected : " + mKeepConnectedReason
+                        + " ; Policies : ", // prefix
+                ")"); // suffix
+        for (int i = NetworkScore.MIN_AGENT_MANAGED_POLICY;
+                i <= NetworkScore.MAX_AGENT_MANAGED_POLICY; ++i) {
+            if (hasPolicy(i)) sj.add(policyNameOf(i));
+        }
+        for (int i = MIN_CS_MANAGED_POLICY; i <= MAX_CS_MANAGED_POLICY; ++i) {
+            if (hasPolicy(i)) sj.add(policyNameOf(i));
+        }
+        return sj.toString();
+    }
+}
diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java
new file mode 100644
index 0000000..acf39f0
--- /dev/null
+++ b/service/src/com/android/server/connectivity/KeepaliveTracker.java
@@ -0,0 +1,768 @@
+/*
+ * 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 com.android.server.connectivity;
+
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.NattSocketKeepalive.NATT_PORT;
+import static android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE;
+import static android.net.SocketKeepalive.BINDER_DIED;
+import static android.net.SocketKeepalive.DATA_RECEIVED;
+import static android.net.SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES;
+import static android.net.SocketKeepalive.ERROR_INVALID_INTERVAL;
+import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS;
+import static android.net.SocketKeepalive.ERROR_INVALID_NETWORK;
+import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
+import static android.net.SocketKeepalive.ERROR_NO_SUCH_SLOT;
+import static android.net.SocketKeepalive.ERROR_STOP_REASON_UNINITIALIZED;
+import static android.net.SocketKeepalive.ERROR_UNSUPPORTED;
+import static android.net.SocketKeepalive.MAX_INTERVAL_SEC;
+import static android.net.SocketKeepalive.MIN_INTERVAL_SEC;
+import static android.net.SocketKeepalive.NO_KEEPALIVE;
+import static android.net.SocketKeepalive.SUCCESS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.ConnectivityResources;
+import android.net.ISocketKeepaliveCallback;
+import android.net.InetAddresses;
+import android.net.InvalidPacketException;
+import android.net.KeepalivePacketData;
+import android.net.NattKeepalivePacketData;
+import android.net.NetworkAgent;
+import android.net.SocketKeepalive.InvalidSocketException;
+import android.net.TcpKeepalivePacketData;
+import android.net.util.KeepaliveUtils;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.IpUtils;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+
+/**
+ * Manages socket keepalive requests.
+ *
+ * Provides methods to stop and start keepalive requests, and keeps track of keepalives across all
+ * networks. This class is tightly coupled to ConnectivityService. It is not thread-safe and its
+ * handle* methods must be called only from the ConnectivityService handler thread.
+ */
+public class KeepaliveTracker {
+
+    private static final String TAG = "KeepaliveTracker";
+    private static final boolean DBG = false;
+
+    public static final String PERMISSION = android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
+
+    /** Keeps track of keepalive requests. */
+    private final HashMap <NetworkAgentInfo, HashMap<Integer, KeepaliveInfo>> mKeepalives =
+            new HashMap<> ();
+    private final Handler mConnectivityServiceHandler;
+    @NonNull
+    private final TcpKeepaliveController mTcpController;
+    @NonNull
+    private final Context mContext;
+
+    // Supported keepalive count for each transport type, can be configured through
+    // config_networkSupportedKeepaliveCount. For better error handling, use
+    // {@link getSupportedKeepalivesForNetworkCapabilities} instead of direct access.
+    @NonNull
+    private final int[] mSupportedKeepalives;
+
+    // Reserved privileged keepalive slots per transport. Caller's permission will be enforced if
+    // the number of remaining keepalive slots is less than or equal to the threshold.
+    private final int mReservedPrivilegedSlots;
+
+    // Allowed unprivileged keepalive slots per uid. Caller's permission will be enforced if
+    // the number of remaining keepalive slots is less than or equal to the threshold.
+    private final int mAllowedUnprivilegedSlotsForUid;
+
+    public KeepaliveTracker(Context context, Handler handler) {
+        mConnectivityServiceHandler = handler;
+        mTcpController = new TcpKeepaliveController(handler);
+        mContext = context;
+        mSupportedKeepalives = KeepaliveUtils.getSupportedKeepalives(mContext);
+
+        // TODO (b/183076074): stop reading legacy resources after migrating overlays
+        final int legacyReservedSlots = mContext.getResources().getInteger(
+                mContext.getResources().getIdentifier(
+                        "config_reservedPrivilegedKeepaliveSlots", "integer", "android"));
+        final int legacyAllowedSlots = mContext.getResources().getInteger(
+                mContext.getResources().getIdentifier(
+                        "config_allowedUnprivilegedKeepalivePerUid", "integer", "android"));
+        final ConnectivityResources res = new ConnectivityResources(mContext);
+        mReservedPrivilegedSlots = Math.min(legacyReservedSlots, res.get().getInteger(
+                R.integer.config_reservedPrivilegedKeepaliveSlots));
+        mAllowedUnprivilegedSlotsForUid = Math.min(legacyAllowedSlots, res.get().getInteger(
+                R.integer.config_allowedUnprivilegedKeepalivePerUid));
+    }
+
+    /**
+     * Tracks information about a socket keepalive.
+     *
+     * All information about this keepalive is known at construction time except the slot number,
+     * which is only returned when the hardware has successfully started the keepalive.
+     */
+    class KeepaliveInfo implements IBinder.DeathRecipient {
+        // Bookkeeping data.
+        private final ISocketKeepaliveCallback mCallback;
+        private final int mUid;
+        private final int mPid;
+        private final boolean mPrivileged;
+        private final NetworkAgentInfo mNai;
+        private final int mType;
+        private final FileDescriptor mFd;
+
+        public static final int TYPE_NATT = 1;
+        public static final int TYPE_TCP = 2;
+
+        // Keepalive slot. A small integer that identifies this keepalive among the ones handled
+        // by this network.
+        private int mSlot = NO_KEEPALIVE;
+
+        // Packet data.
+        private final KeepalivePacketData mPacket;
+        private final int mInterval;
+
+        // Whether the keepalive is started or not. The initial state is NOT_STARTED.
+        private static final int NOT_STARTED = 1;
+        private static final int STARTING = 2;
+        private static final int STARTED = 3;
+        private static final int STOPPING = 4;
+        private int mStartedState = NOT_STARTED;
+        private int mStopReason = ERROR_STOP_REASON_UNINITIALIZED;
+
+        KeepaliveInfo(@NonNull ISocketKeepaliveCallback callback,
+                @NonNull NetworkAgentInfo nai,
+                @NonNull KeepalivePacketData packet,
+                int interval,
+                int type,
+                @Nullable FileDescriptor fd) throws InvalidSocketException {
+            mCallback = callback;
+            mPid = Binder.getCallingPid();
+            mUid = Binder.getCallingUid();
+            mPrivileged = (PERMISSION_GRANTED == mContext.checkPermission(PERMISSION, mPid, mUid));
+
+            mNai = nai;
+            mPacket = packet;
+            mInterval = interval;
+            mType = type;
+
+            // For SocketKeepalive, a dup of fd is kept in mFd so the source port from which the
+            // keepalives are sent cannot be reused by another app even if the fd gets closed by
+            // the user. A null is acceptable here for backward compatibility of PacketKeepalive
+            // API.
+            try {
+                if (fd != null) {
+                    mFd = Os.dup(fd);
+                }  else {
+                    Log.d(TAG, toString() + " calls with null fd");
+                    if (!mPrivileged) {
+                        throw new SecurityException(
+                                "null fd is not allowed for unprivileged access.");
+                    }
+                    if (mType == TYPE_TCP) {
+                        throw new IllegalArgumentException(
+                                "null fd is not allowed for tcp socket keepalives.");
+                    }
+                    mFd = null;
+                }
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot dup fd: ", e);
+                throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+            }
+
+            try {
+                mCallback.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                binderDied();
+            }
+        }
+
+        public NetworkAgentInfo getNai() {
+            return mNai;
+        }
+
+        private String startedStateString(final int state) {
+            switch (state) {
+                case NOT_STARTED : return "NOT_STARTED";
+                case STARTING : return "STARTING";
+                case STARTED : return "STARTED";
+                case STOPPING : return "STOPPING";
+            }
+            throw new IllegalArgumentException("Unknown state");
+        }
+
+        public String toString() {
+            return "KeepaliveInfo ["
+                    + " type=" + mType
+                    + " network=" + mNai.network
+                    + " startedState=" + startedStateString(mStartedState)
+                    + " "
+                    + IpUtils.addressAndPortToString(mPacket.getSrcAddress(), mPacket.getSrcPort())
+                    + "->"
+                    + IpUtils.addressAndPortToString(mPacket.getDstAddress(), mPacket.getDstPort())
+                    + " interval=" + mInterval
+                    + " uid=" + mUid + " pid=" + mPid + " privileged=" + mPrivileged
+                    + " packetData=" + HexDump.toHexString(mPacket.getPacket())
+                    + " ]";
+        }
+
+        /** Called when the application process is killed. */
+        public void binderDied() {
+            stop(BINDER_DIED);
+        }
+
+        void unlinkDeathRecipient() {
+            if (mCallback != null) {
+                mCallback.asBinder().unlinkToDeath(this, 0);
+            }
+        }
+
+        private int checkNetworkConnected() {
+            if (!mNai.networkInfo.isConnectedOrConnecting()) {
+                return ERROR_INVALID_NETWORK;
+            }
+            return SUCCESS;
+        }
+
+        private int checkSourceAddress() {
+            // Check that we have the source address.
+            for (InetAddress address : mNai.linkProperties.getAddresses()) {
+                if (address.equals(mPacket.getSrcAddress())) {
+                    return SUCCESS;
+                }
+            }
+            return ERROR_INVALID_IP_ADDRESS;
+        }
+
+        private int checkInterval() {
+            if (mInterval < MIN_INTERVAL_SEC || mInterval > MAX_INTERVAL_SEC) {
+                return ERROR_INVALID_INTERVAL;
+            }
+            return SUCCESS;
+        }
+
+        private int checkPermission() {
+            final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(mNai);
+            if (networkKeepalives == null) {
+                return ERROR_INVALID_NETWORK;
+            }
+
+            if (mPrivileged) return SUCCESS;
+
+            final int supported = KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(
+                    mSupportedKeepalives, mNai.networkCapabilities);
+
+            int takenUnprivilegedSlots = 0;
+            for (final KeepaliveInfo ki : networkKeepalives.values()) {
+                if (!ki.mPrivileged) ++takenUnprivilegedSlots;
+            }
+            if (takenUnprivilegedSlots > supported - mReservedPrivilegedSlots) {
+                return ERROR_INSUFFICIENT_RESOURCES;
+            }
+
+            // Count unprivileged keepalives for the same uid across networks.
+            int unprivilegedCountSameUid = 0;
+            for (final HashMap<Integer, KeepaliveInfo> kaForNetwork : mKeepalives.values()) {
+                for (final KeepaliveInfo ki : kaForNetwork.values()) {
+                    if (ki.mUid == mUid) {
+                        unprivilegedCountSameUid++;
+                    }
+                }
+            }
+            if (unprivilegedCountSameUid > mAllowedUnprivilegedSlotsForUid) {
+                return ERROR_INSUFFICIENT_RESOURCES;
+            }
+            return SUCCESS;
+        }
+
+        private int checkLimit() {
+            final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(mNai);
+            if (networkKeepalives == null) {
+                return ERROR_INVALID_NETWORK;
+            }
+            final int supported = KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(
+                    mSupportedKeepalives, mNai.networkCapabilities);
+            if (supported == 0) return ERROR_UNSUPPORTED;
+            if (networkKeepalives.size() > supported) return ERROR_INSUFFICIENT_RESOURCES;
+            return SUCCESS;
+        }
+
+        private int isValid() {
+            synchronized (mNai) {
+                int error = checkInterval();
+                if (error == SUCCESS) error = checkLimit();
+                if (error == SUCCESS) error = checkPermission();
+                if (error == SUCCESS) error = checkNetworkConnected();
+                if (error == SUCCESS) error = checkSourceAddress();
+                return error;
+            }
+        }
+
+        void start(int slot) {
+            mSlot = slot;
+            int error = isValid();
+            if (error == SUCCESS) {
+                Log.d(TAG, "Starting keepalive " + mSlot + " on " + mNai.toShortString());
+                switch (mType) {
+                    case TYPE_NATT:
+                        final NattKeepalivePacketData nattData = (NattKeepalivePacketData) mPacket;
+                        mNai.onAddNattKeepalivePacketFilter(slot, nattData);
+                        mNai.onStartNattSocketKeepalive(slot, mInterval, nattData);
+                        break;
+                    case TYPE_TCP:
+                        try {
+                            mTcpController.startSocketMonitor(mFd, this, mSlot);
+                        } catch (InvalidSocketException e) {
+                            handleStopKeepalive(mNai, mSlot, ERROR_INVALID_SOCKET);
+                            return;
+                        }
+                        final TcpKeepalivePacketData tcpData = (TcpKeepalivePacketData) mPacket;
+                        mNai.onAddTcpKeepalivePacketFilter(slot, tcpData);
+                        // TODO: check result from apf and notify of failure as needed.
+                        mNai.onStartTcpSocketKeepalive(slot, mInterval, tcpData);
+                        break;
+                    default:
+                        Log.wtf(TAG, "Starting keepalive with unknown type: " + mType);
+                        handleStopKeepalive(mNai, mSlot, error);
+                        return;
+                }
+                mStartedState = STARTING;
+            } else {
+                handleStopKeepalive(mNai, mSlot, error);
+                return;
+            }
+        }
+
+        void stop(int reason) {
+            int uid = Binder.getCallingUid();
+            if (uid != mUid && uid != Process.SYSTEM_UID) {
+                if (DBG) {
+                    Log.e(TAG, "Cannot stop unowned keepalive " + mSlot + " on " + mNai.network);
+                }
+            }
+            // Ignore the case when the network disconnects immediately after stop() has been
+            // called and the keepalive code is waiting for the response from the modem. This
+            // might happen when the caller listens for a lower-layer network disconnect
+            // callback and stop the keepalive at that time. But the stop() races with the
+            // stop() generated in ConnectivityService network disconnection code path.
+            if (mStartedState == STOPPING && reason == ERROR_INVALID_NETWORK) return;
+
+            // Store the reason of stopping, and report it after the keepalive is fully stopped.
+            if (mStopReason != ERROR_STOP_REASON_UNINITIALIZED) {
+                throw new IllegalStateException("Unexpected stop reason: " + mStopReason);
+            }
+            mStopReason = reason;
+            Log.d(TAG, "Stopping keepalive " + mSlot + " on " + mNai.toShortString()
+                    + ": " + reason);
+            switch (mStartedState) {
+                case NOT_STARTED:
+                    // Remove the reference of the keepalive that meet error before starting,
+                    // e.g. invalid parameter.
+                    cleanupStoppedKeepalive(mNai, mSlot);
+                    break;
+                default:
+                    mStartedState = STOPPING;
+                    switch (mType) {
+                        case TYPE_TCP:
+                            mTcpController.stopSocketMonitor(mSlot);
+                            // fall through
+                        case TYPE_NATT:
+                            mNai.onStopSocketKeepalive(mSlot);
+                            mNai.onRemoveKeepalivePacketFilter(mSlot);
+                            break;
+                        default:
+                            Log.wtf(TAG, "Stopping keepalive with unknown type: " + mType);
+                    }
+            }
+
+            // Close the duplicated fd that maintains the lifecycle of socket whenever
+            // keepalive is running.
+            if (mFd != null) {
+                try {
+                    Os.close(mFd);
+                } catch (ErrnoException e) {
+                    // This should not happen since system server controls the lifecycle of fd when
+                    // keepalive offload is running.
+                    Log.wtf(TAG, "Error closing fd for keepalive " + mSlot + ": " + e);
+                }
+            }
+        }
+
+        void onFileDescriptorInitiatedStop(final int socketKeepaliveReason) {
+            handleStopKeepalive(mNai, mSlot, socketKeepaliveReason);
+        }
+    }
+
+    void notifyErrorCallback(ISocketKeepaliveCallback cb, int error) {
+        if (DBG) Log.w(TAG, "Sending onError(" + error + ") callback");
+        try {
+            cb.onError(error);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Discarded onError(" + error + ") callback");
+        }
+    }
+
+    private  int findFirstFreeSlot(NetworkAgentInfo nai) {
+        HashMap networkKeepalives = mKeepalives.get(nai);
+        if (networkKeepalives == null) {
+            networkKeepalives = new HashMap<Integer, KeepaliveInfo>();
+            mKeepalives.put(nai, networkKeepalives);
+        }
+
+        // Find the lowest-numbered free slot. Slot numbers start from 1, because that's what two
+        // separate chipset implementations independently came up with.
+        int slot;
+        for (slot = 1; slot <= networkKeepalives.size(); slot++) {
+            if (networkKeepalives.get(slot) == null) {
+                return slot;
+            }
+        }
+        return slot;
+    }
+
+    public void handleStartKeepalive(Message message) {
+        KeepaliveInfo ki = (KeepaliveInfo) message.obj;
+        NetworkAgentInfo nai = ki.getNai();
+        int slot = findFirstFreeSlot(nai);
+        mKeepalives.get(nai).put(slot, ki);
+        ki.start(slot);
+    }
+
+    public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) {
+        final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
+        if (networkKeepalives != null) {
+            final ArrayList<KeepaliveInfo> kalist = new ArrayList(networkKeepalives.values());
+            for (KeepaliveInfo ki : kalist) {
+                ki.stop(reason);
+                // Clean up keepalives since the network agent is disconnected and unable to pass
+                // back asynchronous result of stop().
+                cleanupStoppedKeepalive(nai, ki.mSlot);
+            }
+        }
+    }
+
+    public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) {
+        final String networkName = NetworkAgentInfo.toShortString(nai);
+        HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
+        if (networkKeepalives == null) {
+            Log.e(TAG, "Attempt to stop keepalive on nonexistent network " + networkName);
+            return;
+        }
+        KeepaliveInfo ki = networkKeepalives.get(slot);
+        if (ki == null) {
+            Log.e(TAG, "Attempt to stop nonexistent keepalive " + slot + " on " + networkName);
+            return;
+        }
+        ki.stop(reason);
+        // Clean up keepalives will be done as a result of calling ki.stop() after the slots are
+        // freed.
+    }
+
+    private void cleanupStoppedKeepalive(NetworkAgentInfo nai, int slot) {
+        final String networkName = NetworkAgentInfo.toShortString(nai);
+        HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
+        if (networkKeepalives == null) {
+            Log.e(TAG, "Attempt to remove keepalive on nonexistent network " + networkName);
+            return;
+        }
+        KeepaliveInfo ki = networkKeepalives.get(slot);
+        if (ki == null) {
+            Log.e(TAG, "Attempt to remove nonexistent keepalive " + slot + " on " + networkName);
+            return;
+        }
+
+        // Remove the keepalive from hash table so the slot can be considered available when reusing
+        // it.
+        networkKeepalives.remove(slot);
+        Log.d(TAG, "Remove keepalive " + slot + " on " + networkName + ", "
+                + networkKeepalives.size() + " remains.");
+        if (networkKeepalives.isEmpty()) {
+            mKeepalives.remove(nai);
+        }
+
+        // Notify app that the keepalive is stopped.
+        final int reason = ki.mStopReason;
+        if (reason == SUCCESS) {
+            try {
+                ki.mCallback.onStopped();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Discarded onStop callback: " + reason);
+            }
+        } else if (reason == DATA_RECEIVED) {
+            try {
+                ki.mCallback.onDataReceived();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Discarded onDataReceived callback: " + reason);
+            }
+        } else if (reason == ERROR_STOP_REASON_UNINITIALIZED) {
+            throw new IllegalStateException("Unexpected stop reason: " + reason);
+        } else if (reason == ERROR_NO_SUCH_SLOT) {
+            throw new IllegalStateException("No such slot: " + reason);
+        } else {
+            notifyErrorCallback(ki.mCallback, reason);
+        }
+
+        ki.unlinkDeathRecipient();
+    }
+
+    public void handleCheckKeepalivesStillValid(NetworkAgentInfo nai) {
+        HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
+        if (networkKeepalives != null) {
+            ArrayList<Pair<Integer, Integer>> invalidKeepalives = new ArrayList<>();
+            for (int slot : networkKeepalives.keySet()) {
+                int error = networkKeepalives.get(slot).isValid();
+                if (error != SUCCESS) {
+                    invalidKeepalives.add(Pair.create(slot, error));
+                }
+            }
+            for (Pair<Integer, Integer> slotAndError: invalidKeepalives) {
+                handleStopKeepalive(nai, slotAndError.first, slotAndError.second);
+            }
+        }
+    }
+
+    /** Handle keepalive events from lower layer. */
+    public void handleEventSocketKeepalive(@NonNull NetworkAgentInfo nai, int slot, int reason) {
+        KeepaliveInfo ki = null;
+        try {
+            ki = mKeepalives.get(nai).get(slot);
+        } catch(NullPointerException e) {}
+        if (ki == null) {
+            Log.e(TAG, "Event " + NetworkAgent.EVENT_SOCKET_KEEPALIVE + "," + slot + "," + reason
+                    + " for unknown keepalive " + slot + " on " + nai.toShortString());
+            return;
+        }
+
+        // This can be called in a number of situations :
+        // - startedState is STARTING.
+        //   - reason is SUCCESS => go to STARTED.
+        //   - reason isn't SUCCESS => it's an error starting. Go to NOT_STARTED and stop keepalive.
+        // - startedState is STARTED.
+        //   - reason is SUCCESS => it's a success stopping. Go to NOT_STARTED and stop keepalive.
+        //   - reason isn't SUCCESS => it's an error in exec. Go to NOT_STARTED and stop keepalive.
+        // The control is not supposed to ever come here if the state is NOT_STARTED. This is
+        // because in NOT_STARTED state, the code will switch to STARTING before sending messages
+        // to start, and the only way to NOT_STARTED is this function, through the edges outlined
+        // above : in all cases, keepalive gets stopped and can't restart without going into
+        // STARTING as messages are ordered. This also depends on the hardware processing the
+        // messages in order.
+        // TODO : clarify this code and get rid of mStartedState. Using a StateMachine is an
+        // option.
+        if (KeepaliveInfo.STARTING == ki.mStartedState) {
+            if (SUCCESS == reason) {
+                // Keepalive successfully started.
+                Log.d(TAG, "Started keepalive " + slot + " on " + nai.toShortString());
+                ki.mStartedState = KeepaliveInfo.STARTED;
+                try {
+                    ki.mCallback.onStarted(slot);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "Discarded onStarted(" + slot + ") callback");
+                }
+            } else {
+                Log.d(TAG, "Failed to start keepalive " + slot + " on " + nai.toShortString()
+                        + ": " + reason);
+                // The message indicated some error trying to start: do call handleStopKeepalive.
+                handleStopKeepalive(nai, slot, reason);
+            }
+        } else if (KeepaliveInfo.STOPPING == ki.mStartedState) {
+            // The message indicated result of stopping : clean up keepalive slots.
+            Log.d(TAG, "Stopped keepalive " + slot + " on " + nai.toShortString()
+                    + " stopped: " + reason);
+            ki.mStartedState = KeepaliveInfo.NOT_STARTED;
+            cleanupStoppedKeepalive(nai, slot);
+        } else {
+            Log.wtf(TAG, "Event " + NetworkAgent.EVENT_SOCKET_KEEPALIVE + "," + slot + "," + reason
+                    + " for keepalive in wrong state: " + ki.toString());
+        }
+    }
+
+    /**
+     * Called when requesting that keepalives be started on a IPsec NAT-T socket. See
+     * {@link android.net.SocketKeepalive}.
+     **/
+    public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+            @Nullable FileDescriptor fd,
+            int intervalSeconds,
+            @NonNull ISocketKeepaliveCallback cb,
+            @NonNull String srcAddrString,
+            int srcPort,
+            @NonNull String dstAddrString,
+            int dstPort) {
+        if (nai == null) {
+            notifyErrorCallback(cb, ERROR_INVALID_NETWORK);
+            return;
+        }
+
+        InetAddress srcAddress, dstAddress;
+        try {
+            srcAddress = InetAddresses.parseNumericAddress(srcAddrString);
+            dstAddress = InetAddresses.parseNumericAddress(dstAddrString);
+        } catch (IllegalArgumentException e) {
+            notifyErrorCallback(cb, ERROR_INVALID_IP_ADDRESS);
+            return;
+        }
+
+        KeepalivePacketData packet;
+        try {
+            packet = NattKeepalivePacketData.nattKeepalivePacket(
+                    srcAddress, srcPort, dstAddress, NATT_PORT);
+        } catch (InvalidPacketException e) {
+            notifyErrorCallback(cb, e.getError());
+            return;
+        }
+        KeepaliveInfo ki = null;
+        try {
+            ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds,
+                    KeepaliveInfo.TYPE_NATT, fd);
+        } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
+            Log.e(TAG, "Fail to construct keepalive", e);
+            notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
+            return;
+        }
+        Log.d(TAG, "Created keepalive: " + ki.toString());
+        mConnectivityServiceHandler.obtainMessage(
+                NetworkAgent.CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
+    }
+
+    /**
+     * Called by ConnectivityService to start TCP keepalive on a file descriptor.
+     *
+     * In order to offload keepalive for application correctly, sequence number, ack number and
+     * other fields are needed to form the keepalive packet. Thus, this function synchronously
+     * puts the socket into repair mode to get the necessary information. After the socket has been
+     * put into repair mode, the application cannot access the socket until reverted to normal.
+     *
+     * See {@link android.net.SocketKeepalive}.
+     **/
+    public void startTcpKeepalive(@Nullable NetworkAgentInfo nai,
+            @NonNull FileDescriptor fd,
+            int intervalSeconds,
+            @NonNull ISocketKeepaliveCallback cb) {
+        if (nai == null) {
+            notifyErrorCallback(cb, ERROR_INVALID_NETWORK);
+            return;
+        }
+
+        final TcpKeepalivePacketData packet;
+        try {
+            packet = TcpKeepaliveController.getTcpKeepalivePacket(fd);
+        } catch (InvalidSocketException e) {
+            notifyErrorCallback(cb, e.error);
+            return;
+        } catch (InvalidPacketException e) {
+            notifyErrorCallback(cb, e.getError());
+            return;
+        }
+        KeepaliveInfo ki = null;
+        try {
+            ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds,
+                    KeepaliveInfo.TYPE_TCP, fd);
+        } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
+            Log.e(TAG, "Fail to construct keepalive e=" + e);
+            notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
+            return;
+        }
+        Log.d(TAG, "Created keepalive: " + ki.toString());
+        mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
+    }
+
+   /**
+    * Called when requesting that keepalives be started on a IPsec NAT-T socket. This function is
+    * identical to {@link #startNattKeepalive}, but also takes a {@code resourceId}, which is the
+    * resource index bound to the {@link UdpEncapsulationSocket} when creating by
+    * {@link com.android.server.IpSecService} to verify whether the given
+    * {@link UdpEncapsulationSocket} is legitimate.
+    **/
+    public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+            @Nullable FileDescriptor fd,
+            int resourceId,
+            int intervalSeconds,
+            @NonNull ISocketKeepaliveCallback cb,
+            @NonNull String srcAddrString,
+            @NonNull String dstAddrString,
+            int dstPort) {
+        // Ensure that the socket is created by IpSecService.
+        if (!isNattKeepaliveSocketValid(fd, resourceId)) {
+            notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
+        }
+
+        // Get src port to adopt old API.
+        int srcPort = 0;
+        try {
+            final SocketAddress srcSockAddr = Os.getsockname(fd);
+            srcPort = ((InetSocketAddress) srcSockAddr).getPort();
+        } catch (ErrnoException e) {
+            notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
+        }
+
+        // Forward request to old API.
+        startNattKeepalive(nai, fd, intervalSeconds, cb, srcAddrString, srcPort,
+                dstAddrString, dstPort);
+    }
+
+    /**
+     * Verify if the IPsec NAT-T file descriptor and resource Id hold for IPsec keepalive is valid.
+     **/
+    public static boolean isNattKeepaliveSocketValid(@Nullable FileDescriptor fd, int resourceId) {
+        // TODO: 1. confirm whether the fd is called from system api or created by IpSecService.
+        //       2. If the fd is created from the system api, check that it's bounded. And
+        //          call dup to keep the fd open.
+        //       3. If the fd is created from IpSecService, check if the resource ID is valid. And
+        //          hold the resource needed in IpSecService.
+        if (null == fd) {
+            return false;
+        }
+        return true;
+    }
+
+    public void dump(IndentingPrintWriter pw) {
+        pw.println("Supported Socket keepalives: " + Arrays.toString(mSupportedKeepalives));
+        pw.println("Reserved Privileged keepalives: " + mReservedPrivilegedSlots);
+        pw.println("Allowed Unprivileged keepalives per uid: " + mAllowedUnprivilegedSlotsForUid);
+        pw.println("Socket keepalives:");
+        pw.increaseIndent();
+        for (NetworkAgentInfo nai : mKeepalives.keySet()) {
+            pw.println(nai.toShortString());
+            pw.increaseIndent();
+            for (int slot : mKeepalives.get(nai).keySet()) {
+                KeepaliveInfo ki = mKeepalives.get(nai).get(slot);
+                pw.println(slot + ": " + ki.toString());
+            }
+            pw.decreaseIndent();
+        }
+        pw.decreaseIndent();
+    }
+}
diff --git a/service/src/com/android/server/connectivity/LingerMonitor.java b/service/src/com/android/server/connectivity/LingerMonitor.java
new file mode 100644
index 0000000..032612c
--- /dev/null
+++ b/service/src/com/android/server/connectivity/LingerMonitor.java
@@ -0,0 +1,330 @@
+/*
+ * 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.connectivity;
+
+import static android.net.ConnectivityManager.NETID_UNSET;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.ConnectivityResources;
+import android.net.NetworkCapabilities;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.MessageUtils;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+/**
+ * Class that monitors default network linger events and possibly notifies the user of network
+ * switches.
+ *
+ * This class is not thread-safe and all its methods must be called on the ConnectivityService
+ * handler thread.
+ */
+public class LingerMonitor {
+
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+    private static final String TAG = LingerMonitor.class.getSimpleName();
+
+    public static final int DEFAULT_NOTIFICATION_DAILY_LIMIT = 3;
+    public static final long DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS = DateUtils.MINUTE_IN_MILLIS;
+
+    private static final HashMap<String, Integer> TRANSPORT_NAMES = makeTransportToNameMap();
+    @VisibleForTesting
+    public static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName(
+            "com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity"));
+
+    @VisibleForTesting
+    public static final int NOTIFY_TYPE_NONE         = 0;
+    public static final int NOTIFY_TYPE_NOTIFICATION = 1;
+    public static final int NOTIFY_TYPE_TOAST        = 2;
+
+    private static SparseArray<String> sNotifyTypeNames = MessageUtils.findMessageNames(
+            new Class[] { LingerMonitor.class }, new String[]{ "NOTIFY_TYPE_" });
+
+    private final Context mContext;
+    final Resources mResources;
+    private final NetworkNotificationManager mNotifier;
+    private final int mDailyLimit;
+    private final long mRateLimitMillis;
+
+    private long mFirstNotificationMillis;
+    private long mLastNotificationMillis;
+    private int mNotificationCounter;
+
+    /** Current notifications. Maps the netId we switched away from to the netId we switched to. */
+    private final SparseIntArray mNotifications = new SparseIntArray();
+
+    /** Whether we ever notified that we switched away from a particular network. */
+    private final SparseBooleanArray mEverNotified = new SparseBooleanArray();
+
+    public LingerMonitor(Context context, NetworkNotificationManager notifier,
+            int dailyLimit, long rateLimitMillis) {
+        mContext = context;
+        mResources = new ConnectivityResources(mContext).get();
+        mNotifier = notifier;
+        mDailyLimit = dailyLimit;
+        mRateLimitMillis = rateLimitMillis;
+        // Ensure that (now - mLastNotificationMillis) >= rateLimitMillis at first
+        mLastNotificationMillis = -rateLimitMillis;
+    }
+
+    private static HashMap<String, Integer> makeTransportToNameMap() {
+        SparseArray<String> numberToName = MessageUtils.findMessageNames(
+            new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" });
+        HashMap<String, Integer> nameToNumber = new HashMap<>();
+        for (int i = 0; i < numberToName.size(); i++) {
+            // MessageUtils will fail to initialize if there are duplicate constant values, so there
+            // are no duplicates here.
+            nameToNumber.put(numberToName.valueAt(i), numberToName.keyAt(i));
+        }
+        return nameToNumber;
+    }
+
+    private static boolean hasTransport(NetworkAgentInfo nai, int transport) {
+        return nai.networkCapabilities.hasTransport(transport);
+    }
+
+    private int getNotificationSource(NetworkAgentInfo toNai) {
+        for (int i = 0; i < mNotifications.size(); i++) {
+            if (mNotifications.valueAt(i) == toNai.network.getNetId()) {
+                return mNotifications.keyAt(i);
+            }
+        }
+        return NETID_UNSET;
+    }
+
+    private boolean everNotified(NetworkAgentInfo nai) {
+        return mEverNotified.get(nai.network.getNetId(), false);
+    }
+
+    @VisibleForTesting
+    public boolean isNotificationEnabled(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+        // TODO: Evaluate moving to CarrierConfigManager.
+        String[] notifySwitches = mResources.getStringArray(R.array.config_networkNotifySwitches);
+
+        if (VDBG) {
+            Log.d(TAG, "Notify on network switches: " + Arrays.toString(notifySwitches));
+        }
+
+        for (String notifySwitch : notifySwitches) {
+            if (TextUtils.isEmpty(notifySwitch)) continue;
+            String[] transports = notifySwitch.split("-", 2);
+            if (transports.length != 2) {
+                Log.e(TAG, "Invalid network switch notification configuration: " + notifySwitch);
+                continue;
+            }
+            int fromTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[0]);
+            int toTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[1]);
+            if (hasTransport(fromNai, fromTransport) && hasTransport(toNai, toTransport)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private void showNotification(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+        mNotifier.showNotification(fromNai.network.getNetId(), NotificationType.NETWORK_SWITCH,
+                fromNai, toNai, createNotificationIntent(), true);
+    }
+
+    @VisibleForTesting
+    protected PendingIntent createNotificationIntent() {
+        return PendingIntent.getActivity(
+                mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
+                0 /* requestCode */,
+                CELLULAR_SETTINGS,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+    }
+
+    // Removes any notification that was put up as a result of switching to nai.
+    private void maybeStopNotifying(NetworkAgentInfo nai) {
+        int fromNetId = getNotificationSource(nai);
+        if (fromNetId != NETID_UNSET) {
+            mNotifications.delete(fromNetId);
+            mNotifier.clearNotification(fromNetId);
+            // Toasts can't be deleted.
+        }
+    }
+
+    // Notify the user of a network switch using a notification or a toast.
+    private void notify(NetworkAgentInfo fromNai, NetworkAgentInfo toNai, boolean forceToast) {
+        int notifyType = mResources.getInteger(R.integer.config_networkNotifySwitchType);
+        if (notifyType == NOTIFY_TYPE_NOTIFICATION && forceToast) {
+            notifyType = NOTIFY_TYPE_TOAST;
+        }
+
+        if (VDBG) {
+            Log.d(TAG, "Notify type: " + sNotifyTypeNames.get(notifyType, "" + notifyType));
+        }
+
+        switch (notifyType) {
+            case NOTIFY_TYPE_NONE:
+                return;
+            case NOTIFY_TYPE_NOTIFICATION:
+                showNotification(fromNai, toNai);
+                break;
+            case NOTIFY_TYPE_TOAST:
+                mNotifier.showToast(fromNai, toNai);
+                break;
+            default:
+                Log.e(TAG, "Unknown notify type " + notifyType);
+                return;
+        }
+
+        if (DBG) {
+            Log.d(TAG, "Notifying switch from=" + fromNai.toShortString()
+                    + " to=" + toNai.toShortString()
+                    + " type=" + sNotifyTypeNames.get(notifyType, "unknown(" + notifyType + ")"));
+        }
+
+        mNotifications.put(fromNai.network.getNetId(), toNai.network.getNetId());
+        mEverNotified.put(fromNai.network.getNetId(), true);
+    }
+
+    /**
+     * Put up or dismiss a notification or toast for of a change in the default network if needed.
+     *
+     * Putting up a notification when switching from no network to some network is not supported
+     * and as such this method can't be called with a null |fromNai|. It can be called with a
+     * null |toNai| if there isn't a default network any more.
+     *
+     * @param fromNai switching from this NAI
+     * @param toNai switching to this NAI
+     */
+    // The default network changed from fromNai to toNai due to a change in score.
+    public void noteLingerDefaultNetwork(@NonNull final NetworkAgentInfo fromNai,
+            @Nullable final NetworkAgentInfo toNai) {
+        if (VDBG) {
+            Log.d(TAG, "noteLingerDefaultNetwork from=" + fromNai.toShortString()
+                    + " everValidated=" + fromNai.everValidated
+                    + " lastValidated=" + fromNai.lastValidated
+                    + " to=" + toNai.toShortString());
+        }
+
+        // If we are currently notifying the user because the device switched to fromNai, now that
+        // we are switching away from it we should remove the notification. This includes the case
+        // where we switch back to toNai because its score improved again (e.g., because it regained
+        // Internet access).
+        maybeStopNotifying(fromNai);
+
+        // If the network was simply lost (either because it disconnected or because it stopped
+        // being the default with no replacement), then don't show a notification.
+        if (null == toNai) return;
+
+        // If this network never validated, don't notify. Otherwise, we could do things like:
+        //
+        // 1. Unvalidated wifi connects.
+        // 2. Unvalidated mobile data connects.
+        // 3. Cell validates, and we show a notification.
+        // or:
+        // 1. User connects to wireless printer.
+        // 2. User turns on cellular data.
+        // 3. We show a notification.
+        if (!fromNai.everValidated) return;
+
+        // If this network is a captive portal, don't notify. This cannot happen on initial connect
+        // to a captive portal, because the everValidated check above will fail. However, it can
+        // happen if the captive portal reasserts itself (e.g., because its timeout fires). In that
+        // case, as soon as the captive portal reasserts itself, we'll show a sign-in notification.
+        // We don't want to overwrite that notification with this one; the user has already been
+        // notified, and of the two, the captive portal notification is the more useful one because
+        // it allows the user to sign in to the captive portal. In this case, display a toast
+        // in addition to the captive portal notification.
+        //
+        // Note that if the network we switch to is already up when the captive portal reappears,
+        // this won't work because NetworkMonitor tells ConnectivityService that the network is
+        // unvalidated (causing a switch) before asking it to show the sign in notification. In this
+        // case, the toast won't show and we'll only display the sign in notification. This is the
+        // best we can do at this time.
+        boolean forceToast = fromNai.networkCapabilities.hasCapability(
+                NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL);
+
+        // Only show the notification once, in order to avoid irritating the user every time.
+        // TODO: should we do this?
+        if (everNotified(fromNai)) {
+            if (VDBG) {
+                Log.d(TAG, "Not notifying handover from " + fromNai.toShortString()
+                        + ", already notified");
+            }
+            return;
+        }
+
+        // Only show the notification if we switched away because a network became unvalidated, not
+        // because its score changed.
+        // TODO: instead of just skipping notification, keep a note of it, and show it if it becomes
+        // unvalidated.
+        if (fromNai.lastValidated) return;
+
+        if (!isNotificationEnabled(fromNai, toNai)) return;
+
+        final long now = SystemClock.elapsedRealtime();
+        if (isRateLimited(now) || isAboveDailyLimit(now)) return;
+
+        notify(fromNai, toNai, forceToast);
+    }
+
+    public void noteDisconnect(NetworkAgentInfo nai) {
+        mNotifications.delete(nai.network.getNetId());
+        mEverNotified.delete(nai.network.getNetId());
+        maybeStopNotifying(nai);
+        // No need to cancel notifications on nai: NetworkMonitor does that on disconnect.
+    }
+
+    private boolean isRateLimited(long now) {
+        final long millisSinceLast = now - mLastNotificationMillis;
+        if (millisSinceLast < mRateLimitMillis) {
+            return true;
+        }
+        mLastNotificationMillis = now;
+        return false;
+    }
+
+    private boolean isAboveDailyLimit(long now) {
+        if (mFirstNotificationMillis == 0) {
+            mFirstNotificationMillis = now;
+        }
+        final long millisSinceFirst = now - mFirstNotificationMillis;
+        if (millisSinceFirst > DateUtils.DAY_IN_MILLIS) {
+            mNotificationCounter = 0;
+            mFirstNotificationMillis = 0;
+        }
+        if (mNotificationCounter >= mDailyLimit) {
+            return true;
+        }
+        mNotificationCounter++;
+        return false;
+    }
+}
diff --git a/service/src/com/android/server/connectivity/MockableSystemProperties.java b/service/src/com/android/server/connectivity/MockableSystemProperties.java
new file mode 100644
index 0000000..a25b89a
--- /dev/null
+++ b/service/src/com/android/server/connectivity/MockableSystemProperties.java
@@ -0,0 +1,34 @@
+/*
+ * 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.connectivity;
+
+import android.os.SystemProperties;
+
+public class MockableSystemProperties {
+
+    public String get(String key) {
+        return SystemProperties.get(key);
+    }
+
+    public int getInt(String key, int def) {
+        return SystemProperties.getInt(key, def);
+    }
+
+    public boolean getBoolean(String key, boolean def) {
+        return SystemProperties.getBoolean(key, def);
+    }
+}
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
new file mode 100644
index 0000000..c66a280
--- /dev/null
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -0,0 +1,523 @@
+/*
+ * 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.connectivity;
+
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+
+import static com.android.net.module.util.CollectionUtils.contains;
+
+import android.annotation.NonNull;
+import android.net.ConnectivityManager;
+import android.net.IDnsResolver;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkInfo;
+import android.net.RouteInfo;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.server.ConnectivityService;
+
+import java.net.Inet6Address;
+import java.util.Objects;
+
+/**
+ * Class to manage a 464xlat CLAT daemon. Nat464Xlat is not thread safe and should be manipulated
+ * from a consistent and unique thread context. It is the responsibility of ConnectivityService to
+ * call into this class from its own Handler thread.
+ *
+ * @hide
+ */
+public class Nat464Xlat {
+    private static final String TAG = Nat464Xlat.class.getSimpleName();
+
+    // This must match the interface prefix in clatd.c.
+    private static final String CLAT_PREFIX = "v4-";
+
+    // The network types on which we will start clatd,
+    // allowing clat only on networks for which we can support IPv6-only.
+    private static final int[] NETWORK_TYPES = {
+        ConnectivityManager.TYPE_MOBILE,
+        ConnectivityManager.TYPE_WIFI,
+        ConnectivityManager.TYPE_ETHERNET,
+    };
+
+    // The network states in which running clatd is supported.
+    private static final NetworkInfo.State[] NETWORK_STATES = {
+        NetworkInfo.State.CONNECTED,
+        NetworkInfo.State.SUSPENDED,
+    };
+
+    private final IDnsResolver mDnsResolver;
+    private final INetd mNetd;
+
+    // The network we're running on, and its type.
+    private final NetworkAgentInfo mNetwork;
+
+    private enum State {
+        IDLE,         // start() not called. Base iface and stacked iface names are null.
+        DISCOVERING,  // same as IDLE, except prefix discovery in progress.
+        STARTING,     // start() called. Base iface and stacked iface names are known.
+        RUNNING,      // start() called, and the stacked iface is known to be up.
+    }
+
+    /**
+     * NAT64 prefix currently in use. Only valid in STARTING or RUNNING states.
+     * Used, among other things, to avoid updates when switching from a prefix learned from one
+     * source (e.g., RA) to the same prefix learned from another source (e.g., RA).
+     */
+    private IpPrefix mNat64PrefixInUse;
+    /** NAT64 prefix (if any) discovered from DNS via RFC 7050. */
+    private IpPrefix mNat64PrefixFromDns;
+    /** NAT64 prefix (if any) learned from the network via RA. */
+    private IpPrefix mNat64PrefixFromRa;
+    private String mBaseIface;
+    private String mIface;
+    private Inet6Address mIPv6Address;
+    private State mState = State.IDLE;
+
+    private boolean mEnableClatOnCellular;
+    private boolean mPrefixDiscoveryRunning;
+
+    public Nat464Xlat(NetworkAgentInfo nai, INetd netd, IDnsResolver dnsResolver,
+            ConnectivityService.Dependencies deps) {
+        mDnsResolver = dnsResolver;
+        mNetd = netd;
+        mNetwork = nai;
+        mEnableClatOnCellular = deps.getCellular464XlatEnabled();
+    }
+
+    /**
+     * Whether to attempt 464xlat on this network. This is true for an IPv6-only network that is
+     * currently connected and where the NetworkAgent has not disabled 464xlat. It is the signal to
+     * enable NAT64 prefix discovery.
+     *
+     * @param nai the NetworkAgentInfo corresponding to the network.
+     * @return true if the network requires clat, false otherwise.
+     */
+    @VisibleForTesting
+    protected boolean requiresClat(NetworkAgentInfo nai) {
+        // TODO: migrate to NetworkCapabilities.TRANSPORT_*.
+        final boolean supported = contains(NETWORK_TYPES, nai.networkInfo.getType());
+        final boolean connected = contains(NETWORK_STATES, nai.networkInfo.getState());
+
+        // Only run clat on networks that have a global IPv6 address and don't have a native IPv4
+        // address.
+        LinkProperties lp = nai.linkProperties;
+        final boolean isIpv6OnlyNetwork = (lp != null) && lp.hasGlobalIpv6Address()
+                && !lp.hasIpv4Address();
+
+        // If the network tells us it doesn't use clat, respect that.
+        final boolean skip464xlat = (nai.netAgentConfig() != null)
+                && nai.netAgentConfig().skip464xlat;
+
+        return supported && connected && isIpv6OnlyNetwork && !skip464xlat
+            && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
+                ? isCellular464XlatEnabled() : true);
+    }
+
+    /**
+     * Whether the clat demon should be started on this network now. This is true if requiresClat is
+     * true and a NAT64 prefix has been discovered.
+     *
+     * @param nai the NetworkAgentInfo corresponding to the network.
+     * @return true if the network should start clat, false otherwise.
+     */
+    @VisibleForTesting
+    protected boolean shouldStartClat(NetworkAgentInfo nai) {
+        LinkProperties lp = nai.linkProperties;
+        return requiresClat(nai) && lp != null && lp.getNat64Prefix() != null;
+    }
+
+    /**
+     * @return true if clatd has been started and has not yet stopped.
+     * A true result corresponds to internal states STARTING and RUNNING.
+     */
+    public boolean isStarted() {
+        return (mState == State.STARTING || mState == State.RUNNING);
+    }
+
+    /**
+     * @return true if clatd has been started but the stacked interface is not yet up.
+     */
+    public boolean isStarting() {
+        return mState == State.STARTING;
+    }
+
+    /**
+     * @return true if clatd has been started and the stacked interface is up.
+     */
+    public boolean isRunning() {
+        return mState == State.RUNNING;
+    }
+
+    /**
+     * Start clatd, register this Nat464Xlat as a network observer for the stacked interface,
+     * and set internal state.
+     */
+    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);
+        }
+        mIface = CLAT_PREFIX + baseIface;
+        mBaseIface = baseIface;
+        mState = State.STARTING;
+        try {
+            mIPv6Address = (Inet6Address) InetAddresses.parseNumericAddress(addrStr);
+        } catch (ClassCastException | IllegalArgumentException | NullPointerException e) {
+            Log.e(TAG, "Invalid IPv6 address " + addrStr);
+        }
+        if (mPrefixDiscoveryRunning && !isPrefixDiscoveryNeeded()) {
+            stopPrefixDiscovery();
+        }
+        if (!mPrefixDiscoveryRunning) {
+            setPrefix64(mNat64PrefixInUse);
+        }
+    }
+
+    /**
+     * Enter running state just after getting confirmation that the stacked interface is up, and
+     * turn ND offload off if on WiFi.
+     */
+    private void enterRunningState() {
+        mState = State.RUNNING;
+    }
+
+    /**
+     * Unregister as a base observer for the stacked interface, and clear internal state.
+     */
+    private void leaveStartedState() {
+        mNat64PrefixInUse = null;
+        mIface = null;
+        mBaseIface = null;
+
+        if (!mPrefixDiscoveryRunning) {
+            setPrefix64(null);
+        }
+
+        if (isPrefixDiscoveryNeeded()) {
+            if (!mPrefixDiscoveryRunning) {
+                startPrefixDiscovery();
+            }
+            mState = State.DISCOVERING;
+        } else {
+            stopPrefixDiscovery();
+            mState = State.IDLE;
+        }
+    }
+
+    @VisibleForTesting
+    protected void start() {
+        if (isStarted()) {
+            Log.e(TAG, "startClat: already started");
+            return;
+        }
+
+        String baseIface = mNetwork.linkProperties.getInterfaceName();
+        if (baseIface == null) {
+            Log.e(TAG, "startClat: Can't start clat on null interface");
+            return;
+        }
+        // TODO: should we only do this if mNetd.clatdStart() succeeds?
+        Log.i(TAG, "Starting clatd on " + baseIface);
+        enterStartingState(baseIface);
+    }
+
+    @VisibleForTesting
+    protected void stop() {
+        if (!isStarted()) {
+            Log.e(TAG, "stopClat: already stopped");
+            return;
+        }
+
+        Log.i(TAG, "Stopping clatd on " + mBaseIface);
+        try {
+            mNetd.clatdStop(mBaseIface);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Error stopping clatd on " + mBaseIface + ": " + e);
+        }
+
+        String iface = mIface;
+        boolean wasRunning = isRunning();
+
+        // Change state before updating LinkProperties. handleUpdateLinkProperties ends up calling
+        // fixupLinkProperties, and if at that time the state is still RUNNING, fixupLinkProperties
+        // would wrongly inform ConnectivityService that there is still a stacked interface.
+        leaveStartedState();
+
+        if (wasRunning) {
+            LinkProperties lp = new LinkProperties(mNetwork.linkProperties);
+            lp.removeStackedLink(iface);
+            mNetwork.connService().handleUpdateLinkProperties(mNetwork, lp);
+        }
+    }
+
+    private void startPrefixDiscovery() {
+        try {
+            mDnsResolver.startPrefix64Discovery(getNetId());
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Error starting prefix discovery on netId " + getNetId() + ": " + e);
+        }
+        mPrefixDiscoveryRunning = true;
+    }
+
+    private void stopPrefixDiscovery() {
+        try {
+            mDnsResolver.stopPrefix64Discovery(getNetId());
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Error stopping prefix discovery on netId " + getNetId() + ": " + e);
+        }
+        mPrefixDiscoveryRunning = false;
+    }
+
+    private boolean isPrefixDiscoveryNeeded() {
+        // If there is no NAT64 prefix in the RA, prefix discovery is always needed. It cannot be
+        // stopped after it succeeds, because stopping it will cause netd to report that the prefix
+        // has been removed, and that will cause us to stop clatd.
+        return requiresClat(mNetwork) && mNat64PrefixFromRa == null;
+    }
+
+    private void setPrefix64(IpPrefix prefix) {
+        final String prefixString = (prefix != null) ? prefix.toString() : "";
+        try {
+            mDnsResolver.setPrefix64(getNetId(), prefixString);
+        } catch (RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Error setting NAT64 prefix on netId " + getNetId() + " to "
+                    + prefix + ": " + e);
+        }
+    }
+
+    private void maybeHandleNat64PrefixChange() {
+        final IpPrefix newPrefix = selectNat64Prefix();
+        if (!Objects.equals(mNat64PrefixInUse, newPrefix)) {
+            Log.d(TAG, "NAT64 prefix changed from " + mNat64PrefixInUse + " to "
+                    + newPrefix);
+            stop();
+            // It's safe to call update here, even though this method is called from update, because
+            // stop() is guaranteed to have moved out of STARTING and RUNNING, which are the only
+            // states in which this method can be called.
+            update();
+        }
+    }
+
+    /**
+     * Starts/stops NAT64 prefix discovery and clatd as necessary.
+     */
+    public void update() {
+        // TODO: turn this class into a proper StateMachine. http://b/126113090
+        switch (mState) {
+            case IDLE:
+                if (isPrefixDiscoveryNeeded()) {
+                    startPrefixDiscovery();  // Enters DISCOVERING state.
+                    mState = State.DISCOVERING;
+                } else if (requiresClat(mNetwork)) {
+                    start();  // Enters STARTING state.
+                }
+                break;
+
+            case DISCOVERING:
+                if (shouldStartClat(mNetwork)) {
+                    // NAT64 prefix detected. Start clatd.
+                    start();  // Enters STARTING state.
+                    return;
+                }
+                if (!requiresClat(mNetwork)) {
+                    // IPv4 address added. Go back to IDLE state.
+                    stopPrefixDiscovery();
+                    mState = State.IDLE;
+                    return;
+                }
+                break;
+
+            case STARTING:
+            case RUNNING:
+                // NAT64 prefix removed, or IPv4 address added.
+                // Stop clatd and go back into DISCOVERING or idle.
+                if (!shouldStartClat(mNetwork)) {
+                    stop();
+                    break;
+                }
+                // Only necessary while clat is actually started.
+                maybeHandleNat64PrefixChange();
+                break;
+        }
+    }
+
+    /**
+     * Picks a NAT64 prefix to use. Always prefers the prefix from the RA if one is received from
+     * both RA and DNS, because the prefix in the RA has better security and updatability, and will
+     * almost always be received first anyway.
+     *
+     * Any network that supports legacy hosts will support discovering the DNS64 prefix via DNS as
+     * well. If the prefix from the RA is withdrawn, fall back to that for reliability purposes.
+     */
+    private IpPrefix selectNat64Prefix() {
+        return mNat64PrefixFromRa != null ? mNat64PrefixFromRa : mNat64PrefixFromDns;
+    }
+
+    public void setNat64PrefixFromRa(IpPrefix prefix) {
+        mNat64PrefixFromRa = prefix;
+    }
+
+    public void setNat64PrefixFromDns(IpPrefix prefix) {
+        mNat64PrefixFromDns = prefix;
+    }
+
+    /**
+     * Copies the stacked clat link in oldLp, if any, to the passed LinkProperties.
+     * This is necessary because the LinkProperties in mNetwork come from the transport layer, which
+     * has no idea that 464xlat is running on top of it.
+     */
+    public void fixupLinkProperties(@NonNull LinkProperties oldLp, @NonNull LinkProperties lp) {
+        // This must be done even if clatd is not running, because otherwise shouldStartClat would
+        // never return true.
+        lp.setNat64Prefix(selectNat64Prefix());
+
+        if (!isRunning()) {
+            return;
+        }
+        if (lp.getAllInterfaceNames().contains(mIface)) {
+            return;
+        }
+
+        Log.d(TAG, "clatd running, updating NAI for " + mIface);
+        for (LinkProperties stacked: oldLp.getStackedLinks()) {
+            if (Objects.equals(mIface, stacked.getInterfaceName())) {
+                lp.addStackedLink(stacked);
+                return;
+            }
+        }
+    }
+
+    private LinkProperties makeLinkProperties(LinkAddress clatAddress) {
+        LinkProperties stacked = new LinkProperties();
+        stacked.setInterfaceName(mIface);
+
+        // Although the clat interface is a point-to-point tunnel, we don't
+        // point the route directly at the interface because some apps don't
+        // understand routes without gateways (see, e.g., http://b/9597256
+        // http://b/9597516). Instead, set the next hop of the route to the
+        // clat IPv4 address itself (for those apps, it doesn't matter what
+        // the IP of the gateway is, only that there is one).
+        RouteInfo ipv4Default = new RouteInfo(
+                new LinkAddress(NetworkStackConstants.IPV4_ADDR_ANY, 0),
+                clatAddress.getAddress(), mIface);
+        stacked.addRoute(ipv4Default);
+        stacked.addLinkAddress(clatAddress);
+        return stacked;
+    }
+
+    private LinkAddress getLinkAddress(String iface) {
+        try {
+            final InterfaceConfigurationParcel config = mNetd.interfaceGetCfg(iface);
+            return new LinkAddress(
+                    InetAddresses.parseNumericAddress(config.ipv4Addr), config.prefixLength);
+        } catch (IllegalArgumentException | RemoteException | ServiceSpecificException e) {
+            Log.e(TAG, "Error getting link properties: " + e);
+            return null;
+        }
+    }
+
+    /**
+     * Adds stacked link on base link and transitions to RUNNING state.
+     */
+    private void handleInterfaceLinkStateChanged(String iface, boolean up) {
+        // TODO: if we call start(), then stop(), then start() again, and the
+        // interfaceLinkStateChanged notification for the first start is delayed past the first
+        // stop, then the code becomes out of sync with system state and will behave incorrectly.
+        //
+        // This is not trivial to fix because:
+        // 1. It is not guaranteed that start() will eventually result in the interface coming up,
+        //    because there could be an error starting clat (e.g., if the interface goes down before
+        //    the packet socket can be bound).
+        // 2. If start is called multiple times, there is nothing in the interfaceLinkStateChanged
+        //    notification that says which start() call the interface was created by.
+        //
+        // Once this code is converted to StateMachine, it will be possible to use deferMessage to
+        // ensure it stays in STARTING state until the interfaceLinkStateChanged notification fires,
+        // and possibly use a timeout (or provide some guarantees at the lower layer) to address #1.
+        if (!isStarting() || !up || !Objects.equals(mIface, iface)) {
+            return;
+        }
+
+        LinkAddress clatAddress = getLinkAddress(iface);
+        if (clatAddress == null) {
+            Log.e(TAG, "clatAddress was null for stacked iface " + iface);
+            return;
+        }
+
+        Log.i(TAG, String.format("interface %s is up, adding stacked link %s on top of %s",
+                mIface, mIface, mBaseIface));
+        enterRunningState();
+        LinkProperties lp = new LinkProperties(mNetwork.linkProperties);
+        lp.addStackedLink(makeLinkProperties(clatAddress));
+        mNetwork.connService().handleUpdateLinkProperties(mNetwork, lp);
+    }
+
+    /**
+     * Removes stacked link on base link and transitions to IDLE state.
+     */
+    private void handleInterfaceRemoved(String iface) {
+        if (!Objects.equals(mIface, iface)) {
+            return;
+        }
+        if (!isRunning()) {
+            return;
+        }
+
+        Log.i(TAG, "interface " + iface + " removed");
+        // If we're running, and the interface was removed, then we didn't call stop(), and it's
+        // likely that clatd crashed. Ensure we call stop() so we can start clatd again. Calling
+        // stop() will also update LinkProperties, and if clatd crashed, the LinkProperties update
+        // will cause ConnectivityService to call start() again.
+        stop();
+    }
+
+    public void interfaceLinkStateChanged(String iface, boolean up) {
+        mNetwork.handler().post(() -> { handleInterfaceLinkStateChanged(iface, up); });
+    }
+
+    public void interfaceRemoved(String iface) {
+        mNetwork.handler().post(() -> handleInterfaceRemoved(iface));
+    }
+
+    @Override
+    public String toString() {
+        return "mBaseIface: " + mBaseIface + ", mIface: " + mIface + ", mState: " + mState;
+    }
+
+    @VisibleForTesting
+    protected int getNetId() {
+        return mNetwork.network.getNetId();
+    }
+
+    @VisibleForTesting
+    protected boolean isCellular464XlatEnabled() {
+        return mEnableClatOnCellular;
+    }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
new file mode 100644
index 0000000..18becd4
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -0,0 +1,1215 @@
+/*
+ * 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.connectivity;
+
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.transportNamesOf;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.CaptivePortalData;
+import android.net.IDnsResolver;
+import android.net.INetd;
+import android.net.INetworkAgent;
+import android.net.INetworkAgentRegistry;
+import android.net.INetworkMonitor;
+import android.net.LinkProperties;
+import android.net.NattKeepalivePacketData;
+import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkMonitorManager;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.net.NetworkStateSnapshot;
+import android.net.QosCallbackException;
+import android.net.QosFilter;
+import android.net.QosFilterParcelable;
+import android.net.QosSession;
+import android.net.TcpKeepalivePacketData;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.telephony.data.EpsBearerQosSessionAttributes;
+import android.telephony.data.NrQosSessionAttributes;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import com.android.internal.util.WakeupMessage;
+import com.android.server.ConnectivityService;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * A bag class used by ConnectivityService for holding a collection of most recent
+ * information published by a particular NetworkAgent as well as the
+ * AsyncChannel/messenger for reaching that NetworkAgent and lists of NetworkRequests
+ * interested in using it.  Default sort order is descending by score.
+ */
+// States of a network:
+// --------------------
+// 1. registered, uncreated, disconnected, unvalidated
+//    This state is entered when a NetworkFactory registers a NetworkAgent in any state except
+//    the CONNECTED state.
+// 2. registered, uncreated, connecting, unvalidated
+//    This state is entered when a registered NetworkAgent for a VPN network transitions to the
+//    CONNECTING state (TODO: go through this state for every network, not just VPNs).
+//    ConnectivityService will tell netd to create the network early in order to add extra UID
+//    routing rules referencing the netID. These rules need to be in place before the network is
+//    connected to avoid racing against client apps trying to connect to a half-setup network.
+// 3. registered, uncreated, connected, unvalidated
+//    This state is entered when a registered NetworkAgent transitions to the CONNECTED state.
+//    ConnectivityService will tell netd to create the network if it was not already created, and
+//    immediately transition to state #4.
+// 4. registered, created, connected, unvalidated
+//    If this network can satisfy the default NetworkRequest, then NetworkMonitor will
+//    probe for Internet connectivity.
+//    If this network cannot satisfy the default NetworkRequest, it will immediately be
+//    transitioned to state #5.
+//    A network may remain in this state if NetworkMonitor fails to find Internet connectivity,
+//    for example:
+//    a. a captive portal is present, or
+//    b. a WiFi router whose Internet backhaul is down, or
+//    c. a wireless connection stops transfering packets temporarily (e.g. device is in elevator
+//       or tunnel) but does not disconnect from the AP/cell tower, or
+//    d. a stand-alone device offering a WiFi AP without an uplink for configuration purposes.
+// 5. registered, created, connected, validated
+//
+// The device's default network connection:
+// ----------------------------------------
+// Networks in states #4 and #5 may be used as a device's default network connection if they
+// satisfy the default NetworkRequest.
+// A network, that satisfies the default NetworkRequest, in state #5 should always be chosen
+// in favor of a network, that satisfies the default NetworkRequest, in state #4.
+// When deciding between two networks, that both satisfy the default NetworkRequest, to select
+// for the default network connection, the one with the higher score should be chosen.
+//
+// When a network disconnects:
+// ---------------------------
+// If a network's transport disappears, for example:
+// a. WiFi turned off, or
+// b. cellular data turned off, or
+// c. airplane mode is turned on, or
+// d. a wireless connection disconnects from AP/cell tower entirely (e.g. device is out of range
+//    of AP for an extended period of time, or switches to another AP without roaming)
+// then that network can transition from any state (#1-#5) to unregistered.  This happens by
+// the transport disconnecting their NetworkAgent's AsyncChannel with ConnectivityManager.
+// ConnectivityService also tells netd to destroy the network.
+//
+// When ConnectivityService disconnects a network:
+// -----------------------------------------------
+// If a network is just connected, ConnectivityService will think it will be used soon, but might
+// not be used. Thus, a 5s timer will be held to prevent the network being torn down immediately.
+// This "nascent" state is implemented by the "lingering" logic below without relating to any
+// request, and is used in some cases where network requests race with network establishment. The
+// nascent state ends when the 5-second timer fires, or as soon as the network satisfies a
+// request, whichever is earlier. In this state, the network is considered in the background.
+//
+// If a network has no chance of satisfying any requests (even if it were to become validated
+// and enter state #5), ConnectivityService will disconnect the NetworkAgent's AsyncChannel.
+//
+// If the network was satisfying a foreground NetworkRequest (i.e. had been the highest scoring that
+// satisfied the NetworkRequest's constraints), but is no longer the highest scoring network for any
+// foreground NetworkRequest, then there will be a 30s pause to allow network communication to be
+// wrapped up rather than abruptly terminated. During this pause the network is said to be
+// "lingering". During this pause if the network begins satisfying a foreground NetworkRequest,
+// ConnectivityService will cancel the future disconnection of the NetworkAgent's AsyncChannel, and
+// the network is no longer considered "lingering". After the linger timer expires, if the network
+// is satisfying one or more background NetworkRequests it is kept up in the background. If it is
+// not, ConnectivityService disconnects the NetworkAgent's AsyncChannel.
+public class NetworkAgentInfo implements Comparable<NetworkAgentInfo>, NetworkRanker.Scoreable {
+
+    @NonNull public NetworkInfo networkInfo;
+    // This Network object should always be used if possible, so as to encourage reuse of the
+    // enclosed socket factory and connection pool.  Avoid creating other Network objects.
+    // This Network object is always valid.
+    @NonNull public final Network network;
+    @NonNull public LinkProperties linkProperties;
+    // This should only be modified by ConnectivityService, via setNetworkCapabilities().
+    // TODO: make this private with a getter.
+    @NonNull public NetworkCapabilities networkCapabilities;
+    @NonNull public final NetworkAgentConfig networkAgentConfig;
+
+    // Underlying networks declared by the agent. Only set if supportsUnderlyingNetworks is true.
+    // The networks in this list might be declared by a VPN app using setUnderlyingNetworks and are
+    // not guaranteed to be current or correct, or even to exist.
+    //
+    // This array is read and iterated on multiple threads with no locking so its contents must
+    // never be modified. When the list of networks changes, replace with a new array, on the
+    // handler thread.
+    public @Nullable volatile Network[] declaredUnderlyingNetworks;
+
+    // The capabilities originally announced by the NetworkAgent, regardless of any capabilities
+    // that were added or removed due to this network's underlying networks.
+    // Only set if #supportsUnderlyingNetworks is true.
+    public @Nullable NetworkCapabilities declaredCapabilities;
+
+    // Indicates if netd has been told to create this Network. From this point on the appropriate
+    // routing rules are setup and routes are added so packets can begin flowing over the Network.
+    // This is a sticky bit; once set it is never cleared.
+    public boolean created;
+    // Set to true after the first time this network is marked as CONNECTED. Once set, the network
+    // shows up in API calls, is able to satisfy NetworkRequests and can become the default network.
+    // This is a sticky bit; once set it is never cleared.
+    public boolean everConnected;
+    // Set to true if this Network successfully passed validation or if it did not satisfy the
+    // default NetworkRequest in which case validation will not be attempted.
+    // This is a sticky bit; once set it is never cleared even if future validation attempts fail.
+    public boolean everValidated;
+
+    // The result of the last validation attempt on this network (true if validated, false if not).
+    public boolean lastValidated;
+
+    // If true, becoming unvalidated will lower the network's score. This is only meaningful if the
+    // system is configured not to do this for certain networks, e.g., if the
+    // config_networkAvoidBadWifi option is set to 0 and the user has not overridden that via
+    // Settings.Global.NETWORK_AVOID_BAD_WIFI.
+    public boolean avoidUnvalidated;
+
+    // Whether a captive portal was ever detected on this network.
+    // This is a sticky bit; once set it is never cleared.
+    public boolean everCaptivePortalDetected;
+
+    // Whether a captive portal was found during the last network validation attempt.
+    public boolean lastCaptivePortalDetected;
+
+    // Set to true when partial connectivity was detected.
+    public boolean partialConnectivity;
+
+    // Delay between when the network is disconnected and when the native network is destroyed.
+    public int teardownDelayMs;
+
+    // Captive portal info of the network from RFC8908, if any.
+    // Obtained by ConnectivityService and merged into NetworkAgent-provided information.
+    public CaptivePortalData capportApiData;
+
+    // The UID of the remote entity that created this Network.
+    public final int creatorUid;
+
+    // Network agent portal info of the network, if any. This information is provided from
+    // non-RFC8908 sources, such as Wi-Fi Passpoint, which can provide information such as Venue
+    // URL, Terms & Conditions URL, and network friendly name.
+    public CaptivePortalData networkAgentPortalData;
+
+    // Networks are lingered when they become unneeded as a result of their NetworkRequests being
+    // satisfied by a higher-scoring network. so as to allow communication to wrap up before the
+    // network is taken down.  This usually only happens to the default network. Lingering ends with
+    // either the linger timeout expiring and the network being taken down, or the network
+    // satisfying a request again.
+    public static class InactivityTimer implements Comparable<InactivityTimer> {
+        public final int requestId;
+        public final long expiryMs;
+
+        public InactivityTimer(int requestId, long expiryMs) {
+            this.requestId = requestId;
+            this.expiryMs = expiryMs;
+        }
+        public boolean equals(Object o) {
+            if (!(o instanceof InactivityTimer)) return false;
+            InactivityTimer other = (InactivityTimer) o;
+            return (requestId == other.requestId) && (expiryMs == other.expiryMs);
+        }
+        public int hashCode() {
+            return Objects.hash(requestId, expiryMs);
+        }
+        public int compareTo(InactivityTimer other) {
+            return (expiryMs != other.expiryMs) ?
+                    Long.compare(expiryMs, other.expiryMs) :
+                    Integer.compare(requestId, other.requestId);
+        }
+        public String toString() {
+            return String.format("%s, expires %dms", requestId,
+                    expiryMs - SystemClock.elapsedRealtime());
+        }
+    }
+
+    /**
+     * Inform ConnectivityService that the network LINGER period has
+     * expired.
+     * obj = this NetworkAgentInfo
+     */
+    public static final int EVENT_NETWORK_LINGER_COMPLETE = 1001;
+
+    /**
+     * Inform ConnectivityService that the agent is half-connected.
+     * arg1 = ARG_AGENT_SUCCESS or ARG_AGENT_FAILURE
+     * obj = NetworkAgentInfo
+     * @hide
+     */
+    public static final int EVENT_AGENT_REGISTERED = 1002;
+
+    /**
+     * Inform ConnectivityService that the agent was disconnected.
+     * obj = NetworkAgentInfo
+     * @hide
+     */
+    public static final int EVENT_AGENT_DISCONNECTED = 1003;
+
+    /**
+     * Argument for EVENT_AGENT_HALF_CONNECTED indicating failure.
+     */
+    public static final int ARG_AGENT_FAILURE = 0;
+
+    /**
+     * Argument for EVENT_AGENT_HALF_CONNECTED indicating success.
+     */
+    public static final int ARG_AGENT_SUCCESS = 1;
+
+    // How long this network should linger for.
+    private int mLingerDurationMs;
+
+    // All inactivity timers for this network, sorted by expiry time. A timer is added whenever
+    // a request is moved to a network with a better score, regardless of whether the network is or
+    // was lingering or not. An inactivity timer is also added when a network connects
+    // without immediately satisfying any requests.
+    // TODO: determine if we can replace this with a smaller or unsorted data structure. (e.g.,
+    // SparseLongArray) combined with the timestamp of when the last timer is scheduled to fire.
+    private final SortedSet<InactivityTimer> mInactivityTimers = new TreeSet<>();
+
+    // For fast lookups. Indexes into mInactivityTimers by request ID.
+    private final SparseArray<InactivityTimer> mInactivityTimerForRequest = new SparseArray<>();
+
+    // Inactivity expiry timer. Armed whenever mInactivityTimers is non-empty, regardless of
+    // whether the network is inactive or not. Always set to the expiry of the mInactivityTimers
+    // that expires last. When the timer fires, all inactivity state is cleared, and if the network
+    // has no requests, it is torn down.
+    private WakeupMessage mInactivityMessage;
+
+    // Inactivity expiry. Holds the expiry time of the inactivity timer, or 0 if the timer is not
+    // armed.
+    private long mInactivityExpiryMs;
+
+    // Whether the network is inactive or not. Must be maintained separately from the above because
+    // it depends on the state of other networks and requests, which only ConnectivityService knows.
+    // (Example: we don't linger a network if it would become the best for a NetworkRequest if it
+    // validated).
+    private boolean mInactive;
+
+    // This represents the quality of the network. As opposed to NetworkScore, FullScore includes
+    // the ConnectivityService-managed bits.
+    private FullScore mScore;
+
+    // The list of NetworkRequests being satisfied by this Network.
+    private final SparseArray<NetworkRequest> mNetworkRequests = new SparseArray<>();
+
+    // How many of the satisfied requests are actual requests and not listens.
+    private int mNumRequestNetworkRequests = 0;
+
+    // How many of the satisfied requests are of type BACKGROUND_REQUEST.
+    private int mNumBackgroundNetworkRequests = 0;
+
+    // The last ConnectivityReport made available for this network. This value is only null before a
+    // report is generated. Once non-null, it will never be null again.
+    @Nullable private ConnectivityReport mConnectivityReport;
+
+    public final INetworkAgent networkAgent;
+    // Only accessed from ConnectivityService handler thread
+    private final AgentDeathMonitor mDeathMonitor = new AgentDeathMonitor();
+
+    public final int factorySerialNumber;
+
+    // Used by ConnectivityService to keep track of 464xlat.
+    public final Nat464Xlat clatd;
+
+    // Set after asynchronous creation of the NetworkMonitor.
+    private volatile NetworkMonitorManager mNetworkMonitor;
+
+    private static final String TAG = ConnectivityService.class.getSimpleName();
+    private static final boolean VDBG = false;
+    private final ConnectivityService mConnService;
+    private final Context mContext;
+    private final Handler mHandler;
+    private final QosCallbackTracker mQosCallbackTracker;
+
+    public NetworkAgentInfo(INetworkAgent na, Network net, NetworkInfo info,
+            @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+            @NonNull NetworkScore score, Context context,
+            Handler handler, NetworkAgentConfig config, ConnectivityService connService, INetd netd,
+            IDnsResolver dnsResolver, int factorySerialNumber, int creatorUid,
+            int lingerDurationMs, QosCallbackTracker qosCallbackTracker,
+            ConnectivityService.Dependencies deps) {
+        Objects.requireNonNull(net);
+        Objects.requireNonNull(info);
+        Objects.requireNonNull(lp);
+        Objects.requireNonNull(nc);
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(config);
+        Objects.requireNonNull(qosCallbackTracker);
+        networkAgent = na;
+        network = net;
+        networkInfo = info;
+        linkProperties = lp;
+        networkCapabilities = nc;
+        networkAgentConfig = config;
+        mConnService = connService;
+        setScore(score); // uses members connService, networkCapabilities and networkAgentConfig
+        clatd = new Nat464Xlat(this, netd, dnsResolver, deps);
+        mContext = context;
+        mHandler = handler;
+        this.factorySerialNumber = factorySerialNumber;
+        this.creatorUid = creatorUid;
+        mLingerDurationMs = lingerDurationMs;
+        mQosCallbackTracker = qosCallbackTracker;
+    }
+
+    private class AgentDeathMonitor implements IBinder.DeathRecipient {
+        @Override
+        public void binderDied() {
+            notifyDisconnected();
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that it was registered, and should be unregistered if it dies.
+     *
+     * Must be called from the ConnectivityService handler thread. A NetworkAgent can only be
+     * registered once.
+     */
+    public void notifyRegistered() {
+        try {
+            networkAgent.asBinder().linkToDeath(mDeathMonitor, 0);
+            networkAgent.onRegistered(new NetworkAgentMessageHandler(mHandler));
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error registering NetworkAgent", e);
+            maybeUnlinkDeathMonitor();
+            mHandler.obtainMessage(EVENT_AGENT_REGISTERED, ARG_AGENT_FAILURE, 0, this)
+                    .sendToTarget();
+            return;
+        }
+
+        mHandler.obtainMessage(EVENT_AGENT_REGISTERED, ARG_AGENT_SUCCESS, 0, this).sendToTarget();
+    }
+
+    /**
+     * Disconnect the NetworkAgent. Must be called from the ConnectivityService handler thread.
+     */
+    public void disconnect() {
+        try {
+            networkAgent.onDisconnected();
+        } catch (RemoteException e) {
+            Log.i(TAG, "Error disconnecting NetworkAgent", e);
+            // Fall through: it's fine if the remote has died
+        }
+
+        notifyDisconnected();
+        maybeUnlinkDeathMonitor();
+    }
+
+    private void maybeUnlinkDeathMonitor() {
+        try {
+            networkAgent.asBinder().unlinkToDeath(mDeathMonitor, 0);
+        } catch (NoSuchElementException e) {
+            // Was not linked: ignore
+        }
+    }
+
+    private void notifyDisconnected() {
+        // Note this may be called multiple times if ConnectivityService disconnects while the
+        // NetworkAgent also dies. ConnectivityService ignores disconnects of already disconnected
+        // agents.
+        mHandler.obtainMessage(EVENT_AGENT_DISCONNECTED, this).sendToTarget();
+    }
+
+    /**
+     * Notify the NetworkAgent that bandwidth update was requested.
+     */
+    public void onBandwidthUpdateRequested() {
+        try {
+            networkAgent.onBandwidthUpdateRequested();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending bandwidth update request event", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that validation status has changed.
+     */
+    public void onValidationStatusChanged(int validationStatus, @Nullable String captivePortalUrl) {
+        try {
+            networkAgent.onValidationStatusChanged(validationStatus, captivePortalUrl);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending validation status change event", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that the acceptUnvalidated setting should be saved.
+     */
+    public void onSaveAcceptUnvalidated(boolean acceptUnvalidated) {
+        try {
+            networkAgent.onSaveAcceptUnvalidated(acceptUnvalidated);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending accept unvalidated event", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that NATT socket keepalive should be started.
+     */
+    public void onStartNattSocketKeepalive(int slot, int intervalDurationMs,
+            @NonNull NattKeepalivePacketData packetData) {
+        try {
+            networkAgent.onStartNattSocketKeepalive(slot, intervalDurationMs, packetData);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending NATT socket keepalive start event", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that TCP socket keepalive should be started.
+     */
+    public void onStartTcpSocketKeepalive(int slot, int intervalDurationMs,
+            @NonNull TcpKeepalivePacketData packetData) {
+        try {
+            networkAgent.onStartTcpSocketKeepalive(slot, intervalDurationMs, packetData);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending TCP socket keepalive start event", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that socket keepalive should be stopped.
+     */
+    public void onStopSocketKeepalive(int slot) {
+        try {
+            networkAgent.onStopSocketKeepalive(slot);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending TCP socket keepalive stop event", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that signal strength thresholds should be updated.
+     */
+    public void onSignalStrengthThresholdsUpdated(@NonNull int[] thresholds) {
+        try {
+            networkAgent.onSignalStrengthThresholdsUpdated(thresholds);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending signal strength thresholds event", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that automatic reconnect should be prevented.
+     */
+    public void onPreventAutomaticReconnect() {
+        try {
+            networkAgent.onPreventAutomaticReconnect();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending prevent automatic reconnect event", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that a NATT keepalive packet filter should be added.
+     */
+    public void onAddNattKeepalivePacketFilter(int slot,
+            @NonNull NattKeepalivePacketData packetData) {
+        try {
+            networkAgent.onAddNattKeepalivePacketFilter(slot, packetData);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending add NATT keepalive packet filter event", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that a TCP keepalive packet filter should be added.
+     */
+    public void onAddTcpKeepalivePacketFilter(int slot,
+            @NonNull TcpKeepalivePacketData packetData) {
+        try {
+            networkAgent.onAddTcpKeepalivePacketFilter(slot, packetData);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending add TCP keepalive packet filter event", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that a keepalive packet filter should be removed.
+     */
+    public void onRemoveKeepalivePacketFilter(int slot) {
+        try {
+            networkAgent.onRemoveKeepalivePacketFilter(slot);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending remove keepalive packet filter event", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that the qos filter should be registered against the given qos
+     * callback id.
+     */
+    public void onQosFilterCallbackRegistered(final int qosCallbackId,
+            final QosFilter qosFilter) {
+        try {
+            networkAgent.onQosFilterCallbackRegistered(qosCallbackId,
+                    new QosFilterParcelable(qosFilter));
+        } catch (final RemoteException e) {
+            Log.e(TAG, "Error registering a qos callback id against a qos filter", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that the given qos callback id should be unregistered.
+     */
+    public void onQosCallbackUnregistered(final int qosCallbackId) {
+        try {
+            networkAgent.onQosCallbackUnregistered(qosCallbackId);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error unregistering a qos callback id", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that the network is successfully connected.
+     */
+    public void onNetworkCreated() {
+        try {
+            networkAgent.onNetworkCreated();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending network created event", e);
+        }
+    }
+
+    /**
+     * Notify the NetworkAgent that the native network has been destroyed.
+     */
+    public void onNetworkDestroyed() {
+        try {
+            networkAgent.onNetworkDestroyed();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending network destroyed event", e);
+        }
+    }
+
+    // TODO: consider moving out of NetworkAgentInfo into its own class
+    private class NetworkAgentMessageHandler extends INetworkAgentRegistry.Stub {
+        private final Handler mHandler;
+
+        private NetworkAgentMessageHandler(Handler handler) {
+            mHandler = handler;
+        }
+
+        @Override
+        public void sendNetworkCapabilities(@NonNull NetworkCapabilities nc) {
+            Objects.requireNonNull(nc);
+            mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED,
+                    new Pair<>(NetworkAgentInfo.this, nc)).sendToTarget();
+        }
+
+        @Override
+        public void sendLinkProperties(@NonNull LinkProperties lp) {
+            Objects.requireNonNull(lp);
+            mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_PROPERTIES_CHANGED,
+                    new Pair<>(NetworkAgentInfo.this, lp)).sendToTarget();
+        }
+
+        @Override
+        public void sendNetworkInfo(@NonNull NetworkInfo info) {
+            Objects.requireNonNull(info);
+            mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_INFO_CHANGED,
+                    new Pair<>(NetworkAgentInfo.this, info)).sendToTarget();
+        }
+
+        @Override
+        public void sendScore(@NonNull final NetworkScore score) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_SCORE_CHANGED,
+                    new Pair<>(NetworkAgentInfo.this, score)).sendToTarget();
+        }
+
+        @Override
+        public void sendExplicitlySelected(boolean explicitlySelected, boolean acceptPartial) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_SET_EXPLICITLY_SELECTED,
+                    explicitlySelected ? 1 : 0, acceptPartial ? 1 : 0,
+                    new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
+        }
+
+        @Override
+        public void sendSocketKeepaliveEvent(int slot, int reason) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_SOCKET_KEEPALIVE,
+                    slot, reason, new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
+        }
+
+        @Override
+        public void sendUnderlyingNetworks(@Nullable List<Network> networks) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_UNDERLYING_NETWORKS_CHANGED,
+                    new Pair<>(NetworkAgentInfo.this, networks)).sendToTarget();
+        }
+
+        @Override
+        public void sendEpsQosSessionAvailable(final int qosCallbackId, final QosSession session,
+                final EpsBearerQosSessionAttributes attributes) {
+            mQosCallbackTracker.sendEventEpsQosSessionAvailable(qosCallbackId, session, attributes);
+        }
+
+        @Override
+        public void sendNrQosSessionAvailable(final int qosCallbackId, final QosSession session,
+                final NrQosSessionAttributes attributes) {
+            mQosCallbackTracker.sendEventNrQosSessionAvailable(qosCallbackId, session, attributes);
+        }
+
+        @Override
+        public void sendQosSessionLost(final int qosCallbackId, final QosSession session) {
+            mQosCallbackTracker.sendEventQosSessionLost(qosCallbackId, session);
+        }
+
+        @Override
+        public void sendQosCallbackError(final int qosCallbackId,
+                @QosCallbackException.ExceptionType final int exceptionType) {
+            mQosCallbackTracker.sendEventQosCallbackError(qosCallbackId, exceptionType);
+        }
+
+        @Override
+        public void sendTeardownDelayMs(int teardownDelayMs) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_TEARDOWN_DELAY_CHANGED,
+                    teardownDelayMs, 0, new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
+        }
+
+        @Override
+        public void sendLingerDuration(final int durationMs) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_LINGER_DURATION_CHANGED,
+                    new Pair<>(NetworkAgentInfo.this, durationMs)).sendToTarget();
+        }
+    }
+
+    /**
+     * Inform NetworkAgentInfo that a new NetworkMonitor was created.
+     */
+    public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) {
+        mNetworkMonitor = new NetworkMonitorManager(networkMonitor);
+    }
+
+    /**
+     * Set the NetworkCapabilities on this NetworkAgentInfo. Also attempts to notify NetworkMonitor
+     * of the new capabilities, if NetworkMonitor has been created.
+     *
+     * <p>If {@link NetworkMonitor#notifyNetworkCapabilitiesChanged(NetworkCapabilities)} fails,
+     * the exception is logged but not reported to callers.
+     *
+     * @return the old capabilities of this network.
+     */
+    @NonNull public synchronized NetworkCapabilities getAndSetNetworkCapabilities(
+            @NonNull final NetworkCapabilities nc) {
+        final NetworkCapabilities oldNc = networkCapabilities;
+        networkCapabilities = nc;
+        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, everValidatedForYield(),
+                yieldToBadWiFi());
+        final NetworkMonitorManager nm = mNetworkMonitor;
+        if (nm != null) {
+            nm.notifyNetworkCapabilitiesChanged(nc);
+        }
+        return oldNc;
+    }
+
+    private boolean yieldToBadWiFi() {
+        // Only cellular networks yield to bad wifi
+        return networkCapabilities.hasTransport(TRANSPORT_CELLULAR) && !mConnService.avoidBadWifi();
+    }
+
+    public ConnectivityService connService() {
+        return mConnService;
+    }
+
+    public NetworkAgentConfig netAgentConfig() {
+        return networkAgentConfig;
+    }
+
+    public Handler handler() {
+        return mHandler;
+    }
+
+    public Network network() {
+        return network;
+    }
+
+    /**
+     * Get the NetworkMonitorManager in this NetworkAgentInfo.
+     *
+     * <p>This will be null before {@link #onNetworkMonitorCreated(INetworkMonitor)} is called.
+     */
+    public NetworkMonitorManager networkMonitor() {
+        return mNetworkMonitor;
+    }
+
+    // Functions for manipulating the requests satisfied by this network.
+    //
+    // These functions must only called on ConnectivityService's main thread.
+
+    private static final boolean ADD = true;
+    private static final boolean REMOVE = false;
+
+    private void updateRequestCounts(boolean add, NetworkRequest request) {
+        int delta = add ? +1 : -1;
+        switch (request.type) {
+            case REQUEST:
+                mNumRequestNetworkRequests += delta;
+                break;
+
+            case BACKGROUND_REQUEST:
+                mNumRequestNetworkRequests += delta;
+                mNumBackgroundNetworkRequests += delta;
+                break;
+
+            case LISTEN:
+            case LISTEN_FOR_BEST:
+            case TRACK_DEFAULT:
+            case TRACK_SYSTEM_DEFAULT:
+                break;
+
+            case NONE:
+            default:
+                Log.wtf(TAG, "Unhandled request type " + request.type);
+                break;
+        }
+    }
+
+    /**
+     * Add {@code networkRequest} to this network as it's satisfied by this network.
+     * @return true if {@code networkRequest} was added or false if {@code networkRequest} was
+     *         already present.
+     */
+    public boolean addRequest(NetworkRequest networkRequest) {
+        NetworkRequest existing = mNetworkRequests.get(networkRequest.requestId);
+        if (existing == networkRequest) return false;
+        if (existing != null) {
+            // Should only happen if the requestId wraps. If that happens lots of other things will
+            // be broken as well.
+            Log.wtf(TAG, String.format("Duplicate requestId for %s and %s on %s",
+                    networkRequest, existing, toShortString()));
+            updateRequestCounts(REMOVE, existing);
+        }
+        mNetworkRequests.put(networkRequest.requestId, networkRequest);
+        updateRequestCounts(ADD, networkRequest);
+        return true;
+    }
+
+    /**
+     * Remove the specified request from this network.
+     */
+    public void removeRequest(int requestId) {
+        NetworkRequest existing = mNetworkRequests.get(requestId);
+        if (existing == null) return;
+        updateRequestCounts(REMOVE, existing);
+        mNetworkRequests.remove(requestId);
+        if (existing.isRequest()) {
+            unlingerRequest(existing.requestId);
+        }
+    }
+
+    /**
+     * Returns whether this network is currently satisfying the request with the specified ID.
+     */
+    public boolean isSatisfyingRequest(int id) {
+        return mNetworkRequests.get(id) != null;
+    }
+
+    /**
+     * Returns the request at the specified position in the list of requests satisfied by this
+     * network.
+     */
+    public NetworkRequest requestAt(int index) {
+        return mNetworkRequests.valueAt(index);
+    }
+
+    /**
+     * Returns the number of requests currently satisfied by this network for which
+     * {@link android.net.NetworkRequest#isRequest} returns {@code true}.
+     */
+    public int numRequestNetworkRequests() {
+        return mNumRequestNetworkRequests;
+    }
+
+    /**
+     * Returns the number of requests currently satisfied by this network of type
+     * {@link android.net.NetworkRequest.Type.BACKGROUND_REQUEST}.
+     */
+    public int numBackgroundNetworkRequests() {
+        return mNumBackgroundNetworkRequests;
+    }
+
+    /**
+     * Returns the number of foreground requests currently satisfied by this network.
+     */
+    public int numForegroundNetworkRequests() {
+        return mNumRequestNetworkRequests - mNumBackgroundNetworkRequests;
+    }
+
+    /**
+     * Returns the number of requests of any type currently satisfied by this network.
+     */
+    public int numNetworkRequests() {
+        return mNetworkRequests.size();
+    }
+
+    /**
+     * Returns whether the network is a background network. A network is a background network if it
+     * does not have the NET_CAPABILITY_FOREGROUND capability, which implies it is satisfying no
+     * foreground request, is not lingering (i.e. kept for a while after being outscored), and is
+     * not a speculative network (i.e. kept pending validation when validation would have it
+     * outscore another foreground network). That implies it is being kept up by some background
+     * request (otherwise it would be torn down), maybe the mobile always-on request.
+     */
+    public boolean isBackgroundNetwork() {
+        return !isVPN() && numForegroundNetworkRequests() == 0 && mNumBackgroundNetworkRequests > 0
+                && !isLingering();
+    }
+
+    // Does this network satisfy request?
+    public boolean satisfies(NetworkRequest request) {
+        return created &&
+                request.networkCapabilities.satisfiedByNetworkCapabilities(networkCapabilities);
+    }
+
+    public boolean satisfiesImmutableCapabilitiesOf(NetworkRequest request) {
+        return created &&
+                request.networkCapabilities.satisfiedByImmutableNetworkCapabilities(
+                        networkCapabilities);
+    }
+
+    /** Whether this network is a VPN. */
+    public boolean isVPN() {
+        return networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
+    }
+
+    /** Whether this network might have underlying networks. Currently only true for VPNs. */
+    public boolean supportsUnderlyingNetworks() {
+        return isVPN();
+    }
+
+    // Caller must not mutate. This method is called frequently and making a defensive copy
+    // would be too expensive. This is used by NetworkRanker.Scoreable, so it can be compared
+    // against other scoreables.
+    @Override public NetworkCapabilities getCapsNoCopy() {
+        return networkCapabilities;
+    }
+
+    // NetworkRanker.Scoreable
+    @Override public FullScore getScore() {
+        return mScore;
+    }
+
+    // Get the current score for this Network.  This may be modified from what the
+    // NetworkAgent sent, as it has modifiers applied to it.
+    public int getCurrentScore() {
+        return mScore.getLegacyInt();
+    }
+
+    // Get the current score for this Network as if it was validated.  This may be modified from
+    // what the NetworkAgent sent, as it has modifiers applied to it.
+    public int getCurrentScoreAsValidated() {
+        return mScore.getLegacyIntAsValidated();
+    }
+
+    /**
+     * Mix-in the ConnectivityService-managed bits in the score.
+     */
+    public void setScore(final NetworkScore score) {
+        mScore = FullScore.fromNetworkScore(score, networkCapabilities, networkAgentConfig,
+                everValidatedForYield(), yieldToBadWiFi());
+    }
+
+    /**
+     * Update the ConnectivityService-managed bits in the score.
+     *
+     * Call this after updating the network agent config.
+     */
+    public void updateScoreForNetworkAgentUpdate() {
+        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig,
+                everValidatedForYield(), yieldToBadWiFi());
+    }
+
+    private boolean everValidatedForYield() {
+        return everValidated && !avoidUnvalidated;
+    }
+
+    /**
+     * Returns a Scoreable identical to this NAI, but validated.
+     *
+     * This is useful to probe what scoring would be if this network validated, to know
+     * whether to provisionally keep a network that may or may not validate.
+     *
+     * @return a Scoreable identical to this NAI, but validated.
+     */
+    public NetworkRanker.Scoreable getValidatedScoreable() {
+        return new NetworkRanker.Scoreable() {
+            @Override public FullScore getScore() {
+                return mScore.asValidated();
+            }
+
+            @Override public NetworkCapabilities getCapsNoCopy() {
+                return networkCapabilities;
+            }
+        };
+    }
+
+    /**
+     * Return a {@link NetworkStateSnapshot} for this network.
+     */
+    @NonNull
+    public NetworkStateSnapshot getNetworkStateSnapshot() {
+        synchronized (this) {
+            // Network objects are outwardly immutable so there is no point in duplicating.
+            // Duplicating also precludes sharing socket factories and connection pools.
+            final String subscriberId = (networkAgentConfig != null)
+                    ? networkAgentConfig.subscriberId : null;
+            return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities),
+                    new LinkProperties(linkProperties), subscriberId, networkInfo.getType());
+        }
+    }
+
+    /**
+     * Sets the specified requestId to linger on this network for the specified time. Called by
+     * ConnectivityService when any request is moved to another network with a higher score, or
+     * when a network is newly created.
+     *
+     * @param requestId The requestId of the request that no longer need to be served by this
+     *                  network. Or {@link NetworkRequest.REQUEST_ID_NONE} if this is the
+     *                  {@code InactivityTimer} for a newly created network.
+     */
+    // TODO: Consider creating a dedicated function for nascent network, e.g. start/stopNascent.
+    public void lingerRequest(int requestId, long now, long duration) {
+        if (mInactivityTimerForRequest.get(requestId) != null) {
+            // Cannot happen. Once a request is lingering on a particular network, we cannot
+            // re-linger it unless that network becomes the best for that request again, in which
+            // case we should have unlingered it.
+            Log.wtf(TAG, toShortString() + ": request " + requestId + " already lingered");
+        }
+        final long expiryMs = now + duration;
+        InactivityTimer timer = new InactivityTimer(requestId, expiryMs);
+        if (VDBG) Log.d(TAG, "Adding InactivityTimer " + timer + " to " + toShortString());
+        mInactivityTimers.add(timer);
+        mInactivityTimerForRequest.put(requestId, timer);
+    }
+
+    /**
+     * Sets the specified requestId to linger on this network for the timeout set when
+     * initializing or modified by {@link #setLingerDuration(int)}. Called by
+     * ConnectivityService when any request is moved to another network with a higher score.
+     *
+     * @param requestId The requestId of the request that no longer need to be served by this
+     *                  network.
+     * @param now current system timestamp obtained by {@code SystemClock.elapsedRealtime}.
+     */
+    public void lingerRequest(int requestId, long now) {
+        lingerRequest(requestId, now, mLingerDurationMs);
+    }
+
+    /**
+     * Cancel lingering. Called by ConnectivityService when a request is added to this network.
+     * Returns true if the given requestId was lingering on this network, false otherwise.
+     */
+    public boolean unlingerRequest(int requestId) {
+        InactivityTimer timer = mInactivityTimerForRequest.get(requestId);
+        if (timer != null) {
+            if (VDBG) {
+                Log.d(TAG, "Removing InactivityTimer " + timer + " from " + toShortString());
+            }
+            mInactivityTimers.remove(timer);
+            mInactivityTimerForRequest.remove(requestId);
+            return true;
+        }
+        return false;
+    }
+
+    public long getInactivityExpiry() {
+        return mInactivityExpiryMs;
+    }
+
+    public void updateInactivityTimer() {
+        long newExpiry = mInactivityTimers.isEmpty() ? 0 : mInactivityTimers.last().expiryMs;
+        if (newExpiry == mInactivityExpiryMs) return;
+
+        // Even if we're going to reschedule the timer, cancel it first. This is because the
+        // semantics of WakeupMessage guarantee that if cancel is called then the alarm will
+        // never call its callback (handleLingerComplete), even if it has already fired.
+        // WakeupMessage makes no such guarantees about rescheduling a message, so if mLingerMessage
+        // has already been dispatched, rescheduling to some time in the future won't stop it
+        // from calling its callback immediately.
+        if (mInactivityMessage != null) {
+            mInactivityMessage.cancel();
+            mInactivityMessage = null;
+        }
+
+        if (newExpiry > 0) {
+            // If the newExpiry timestamp is in the past, the wakeup message will fire immediately.
+            mInactivityMessage = new WakeupMessage(
+                    mContext, mHandler,
+                    "NETWORK_LINGER_COMPLETE." + network.getNetId() /* cmdName */,
+                    EVENT_NETWORK_LINGER_COMPLETE /* cmd */,
+                    0 /* arg1 (unused) */, 0 /* arg2 (unused) */,
+                    this /* obj (NetworkAgentInfo) */);
+            mInactivityMessage.schedule(newExpiry);
+        }
+
+        mInactivityExpiryMs = newExpiry;
+    }
+
+    public void setInactive() {
+        mInactive = true;
+    }
+
+    public void unsetInactive() {
+        mInactive = false;
+    }
+
+    public boolean isInactive() {
+        return mInactive;
+    }
+
+    public boolean isLingering() {
+        return mInactive && !isNascent();
+    }
+
+    /**
+     * Set the linger duration for this NAI.
+     * @param durationMs The new linger duration, in milliseconds.
+     */
+    public void setLingerDuration(final int durationMs) {
+        final long diff = durationMs - mLingerDurationMs;
+        final ArrayList<InactivityTimer> newTimers = new ArrayList<>();
+        for (final InactivityTimer timer : mInactivityTimers) {
+            if (timer.requestId == NetworkRequest.REQUEST_ID_NONE) {
+                // Don't touch nascent timer, re-add as is.
+                newTimers.add(timer);
+            } else {
+                newTimers.add(new InactivityTimer(timer.requestId, timer.expiryMs + diff));
+            }
+        }
+        mInactivityTimers.clear();
+        mInactivityTimers.addAll(newTimers);
+        updateInactivityTimer();
+        mLingerDurationMs = durationMs;
+    }
+
+    /**
+     * Return whether the network satisfies no request, but is still being kept up
+     * because it has just connected less than
+     * {@code ConnectivityService#DEFAULT_NASCENT_DELAY_MS}ms ago and is thus still considered
+     * nascent. Note that nascent mechanism uses inactivity timer which isn't
+     * associated with a request. Thus, use {@link NetworkRequest#REQUEST_ID_NONE} to identify it.
+     *
+     */
+    public boolean isNascent() {
+        return mInactive && mInactivityTimers.size() == 1
+                && mInactivityTimers.first().requestId == NetworkRequest.REQUEST_ID_NONE;
+    }
+
+    public void clearInactivityState() {
+        if (mInactivityMessage != null) {
+            mInactivityMessage.cancel();
+            mInactivityMessage = null;
+        }
+        mInactivityTimers.clear();
+        mInactivityTimerForRequest.clear();
+        // Sets mInactivityExpiryMs, cancels and nulls out mInactivityMessage.
+        updateInactivityTimer();
+        mInactive = false;
+    }
+
+    public void dumpInactivityTimers(PrintWriter pw) {
+        for (InactivityTimer timer : mInactivityTimers) {
+            pw.println(timer);
+        }
+    }
+
+    /**
+     * Sets the most recent ConnectivityReport for this network.
+     *
+     * <p>This should only be called from the ConnectivityService thread.
+     *
+     * @hide
+     */
+    public void setConnectivityReport(@NonNull ConnectivityReport connectivityReport) {
+        mConnectivityReport = connectivityReport;
+    }
+
+    /**
+     * Returns the most recent ConnectivityReport for this network, or null if none have been
+     * reported yet.
+     *
+     * <p>This should only be called from the ConnectivityService thread.
+     *
+     * @hide
+     */
+    @Nullable
+    public ConnectivityReport getConnectivityReport() {
+        return mConnectivityReport;
+    }
+
+    // TODO: Print shorter members first and only print the boolean variable which value is true
+    // to improve readability.
+    public String toString() {
+        return "NetworkAgentInfo{"
+                + "network{" + network + "}  handle{" + network.getNetworkHandle() + "}  ni{"
+                + networkInfo.toShortString() + "} "
+                + mScore + " "
+                + (isNascent() ? " nascent" : (isLingering() ? " lingering" : ""))
+                + (everValidated ? " everValidated" : "")
+                + (lastValidated ? " lastValidated" : "")
+                + (partialConnectivity ? " partialConnectivity" : "")
+                + (everCaptivePortalDetected ? " everCaptivePortal" : "")
+                + (lastCaptivePortalDetected ? " isCaptivePortal" : "")
+                + (networkAgentConfig.explicitlySelected ? " explicitlySelected" : "")
+                + (networkAgentConfig.acceptUnvalidated ? " acceptUnvalidated" : "")
+                + (networkAgentConfig.acceptPartialConnectivity ? " acceptPartialConnectivity" : "")
+                + (clatd.isStarted() ? " clat{" + clatd + "} " : "")
+                + (declaredUnderlyingNetworks != null
+                        ? " underlying{" + Arrays.toString(declaredUnderlyingNetworks) + "}" : "")
+                + "  lp{" + linkProperties + "}"
+                + "  nc{" + networkCapabilities + "}"
+                + "}";
+    }
+
+    /**
+     * Show a short string representing a Network.
+     *
+     * This is often not enough for debugging purposes for anything complex, but the full form
+     * is very long and hard to read, so this is useful when there isn't a lot of ambiguity.
+     * This represents the network with something like "[100 WIFI|VPN]" or "[108 MOBILE]".
+     */
+    public String toShortString() {
+        return "[" + network.getNetId() + " "
+                + transportNamesOf(networkCapabilities.getTransportTypes()) + "]";
+    }
+
+    // Enables sorting in descending order of score.
+    @Override
+    public int compareTo(NetworkAgentInfo other) {
+        return other.getCurrentScore() - getCurrentScore();
+    }
+
+    /**
+     * Null-guarding version of NetworkAgentInfo#toShortString()
+     */
+    @NonNull
+    public static String toShortString(@Nullable final NetworkAgentInfo nai) {
+        return null != nai ? nai.toShortString() : "[null]";
+    }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkDiagnostics.java b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
new file mode 100644
index 0000000..2e51be3
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
@@ -0,0 +1,757 @@
+/*
+ * 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 com.android.server.connectivity;
+
+import static android.system.OsConstants.*;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.InetAddresses;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.RouteInfo;
+import android.net.TrafficStats;
+import android.net.shared.PrivateDnsConfig;
+import android.net.util.NetworkConstants;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.NetworkStackConstants;
+
+import libcore.io.IoUtils;
+
+import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.NetworkInterface;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * NetworkDiagnostics
+ *
+ * A simple class to diagnose network connectivity fundamentals.  Current
+ * checks performed are:
+ *     - ICMPv4/v6 echo requests for all routers
+ *     - ICMPv4/v6 echo requests for all DNS servers
+ *     - DNS UDP queries to all DNS servers
+ *
+ * Currently unimplemented checks include:
+ *     - report ARP/ND data about on-link neighbors
+ *     - DNS TCP queries to all DNS servers
+ *     - HTTP DIRECT and PROXY checks
+ *     - port 443 blocking/TLS intercept checks
+ *     - QUIC reachability checks
+ *     - MTU checks
+ *
+ * The supplied timeout bounds the entire diagnostic process.  Each specific
+ * check class must implement this upper bound on measurements in whichever
+ * manner is most appropriate and effective.
+ *
+ * @hide
+ */
+public class NetworkDiagnostics {
+    private static final String TAG = "NetworkDiagnostics";
+
+    private static final InetAddress TEST_DNS4 = InetAddresses.parseNumericAddress("8.8.8.8");
+    private static final InetAddress TEST_DNS6 = InetAddresses.parseNumericAddress(
+            "2001:4860:4860::8888");
+
+    // For brevity elsewhere.
+    private static final long now() {
+        return SystemClock.elapsedRealtime();
+    }
+
+    // Values from RFC 1035 section 4.1.1, names from <arpa/nameser.h>.
+    // Should be a member of DnsUdpCheck, but "compiler says no".
+    public static enum DnsResponseCode { NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED };
+
+    private final Network mNetwork;
+    private final LinkProperties mLinkProperties;
+    private final PrivateDnsConfig mPrivateDnsCfg;
+    private final Integer mInterfaceIndex;
+
+    private final long mTimeoutMs;
+    private final long mStartTime;
+    private final long mDeadlineTime;
+
+    // A counter, initialized to the total number of measurements,
+    // so callers can wait for completion.
+    private final CountDownLatch mCountDownLatch;
+
+    public class Measurement {
+        private static final String SUCCEEDED = "SUCCEEDED";
+        private static final String FAILED = "FAILED";
+
+        private boolean succeeded;
+
+        // Package private.  TODO: investigate better encapsulation.
+        String description = "";
+        long startTime;
+        long finishTime;
+        String result = "";
+        Thread thread;
+
+        public boolean checkSucceeded() { return succeeded; }
+
+        void recordSuccess(String msg) {
+            maybeFixupTimes();
+            succeeded = true;
+            result = SUCCEEDED + ": " + msg;
+            if (mCountDownLatch != null) {
+                mCountDownLatch.countDown();
+            }
+        }
+
+        void recordFailure(String msg) {
+            maybeFixupTimes();
+            succeeded = false;
+            result = FAILED + ": " + msg;
+            if (mCountDownLatch != null) {
+                mCountDownLatch.countDown();
+            }
+        }
+
+        private void maybeFixupTimes() {
+            // Allows the caller to just set success/failure and not worry
+            // about also setting the correct finishing time.
+            if (finishTime == 0) { finishTime = now(); }
+
+            // In cases where, for example, a failure has occurred before the
+            // measurement even began, fixup the start time to reflect as much.
+            if (startTime == 0) { startTime = finishTime; }
+        }
+
+        @Override
+        public String toString() {
+            return description + ": " + result + " (" + (finishTime - startTime) + "ms)";
+        }
+    }
+
+    private final Map<InetAddress, Measurement> mIcmpChecks = new HashMap<>();
+    private final Map<Pair<InetAddress, InetAddress>, Measurement> mExplicitSourceIcmpChecks =
+            new HashMap<>();
+    private final Map<InetAddress, Measurement> mDnsUdpChecks = new HashMap<>();
+    private final Map<InetAddress, Measurement> mDnsTlsChecks = new HashMap<>();
+    private final String mDescription;
+
+
+    public NetworkDiagnostics(Network network, LinkProperties lp,
+            @NonNull PrivateDnsConfig privateDnsCfg, long timeoutMs) {
+        mNetwork = network;
+        mLinkProperties = lp;
+        mPrivateDnsCfg = privateDnsCfg;
+        mInterfaceIndex = getInterfaceIndex(mLinkProperties.getInterfaceName());
+        mTimeoutMs = timeoutMs;
+        mStartTime = now();
+        mDeadlineTime = mStartTime + mTimeoutMs;
+
+        // Hardcode measurements to TEST_DNS4 and TEST_DNS6 in order to test off-link connectivity.
+        // We are free to modify mLinkProperties with impunity because ConnectivityService passes us
+        // a copy and not the original object. It's easier to do it this way because we don't need
+        // to check whether the LinkProperties already contains these DNS servers because
+        // LinkProperties#addDnsServer checks for duplicates.
+        if (mLinkProperties.isReachable(TEST_DNS4)) {
+            mLinkProperties.addDnsServer(TEST_DNS4);
+        }
+        // TODO: we could use mLinkProperties.isReachable(TEST_DNS6) here, because we won't set any
+        // DNS servers for which isReachable() is false, but since this is diagnostic code, be extra
+        // careful.
+        if (mLinkProperties.hasGlobalIpv6Address() || mLinkProperties.hasIpv6DefaultRoute()) {
+            mLinkProperties.addDnsServer(TEST_DNS6);
+        }
+
+        for (RouteInfo route : mLinkProperties.getRoutes()) {
+            if (route.hasGateway()) {
+                InetAddress gateway = route.getGateway();
+                prepareIcmpMeasurement(gateway);
+                if (route.isIPv6Default()) {
+                    prepareExplicitSourceIcmpMeasurements(gateway);
+                }
+            }
+        }
+        for (InetAddress nameserver : mLinkProperties.getDnsServers()) {
+            prepareIcmpMeasurement(nameserver);
+            prepareDnsMeasurement(nameserver);
+
+            // Unlike the DnsResolver which doesn't do certificate validation in opportunistic mode,
+            // DoT probes to the DNS servers will fail if certificate validation fails.
+            prepareDnsTlsMeasurement(null /* hostname */, nameserver);
+        }
+
+        for (InetAddress tlsNameserver : mPrivateDnsCfg.ips) {
+            // Reachability check is necessary since when resolving the strict mode hostname,
+            // NetworkMonitor always queries for both A and AAAA records, even if the network
+            // is IPv4-only or IPv6-only.
+            if (mLinkProperties.isReachable(tlsNameserver)) {
+                // If there are IPs, there must have been a name that resolved to them.
+                prepareDnsTlsMeasurement(mPrivateDnsCfg.hostname, tlsNameserver);
+            }
+        }
+
+        mCountDownLatch = new CountDownLatch(totalMeasurementCount());
+
+        startMeasurements();
+
+        mDescription = "ifaces{" + TextUtils.join(",", mLinkProperties.getAllInterfaceNames()) + "}"
+                + " index{" + mInterfaceIndex + "}"
+                + " network{" + mNetwork + "}"
+                + " nethandle{" + mNetwork.getNetworkHandle() + "}";
+    }
+
+    private static Integer getInterfaceIndex(String ifname) {
+        try {
+            NetworkInterface ni = NetworkInterface.getByName(ifname);
+            return ni.getIndex();
+        } catch (NullPointerException | SocketException e) {
+            return null;
+        }
+    }
+
+    private static String socketAddressToString(@NonNull SocketAddress sockAddr) {
+        // The default toString() implementation is not the prettiest.
+        InetSocketAddress inetSockAddr = (InetSocketAddress) sockAddr;
+        InetAddress localAddr = inetSockAddr.getAddress();
+        return String.format(
+                (localAddr instanceof Inet6Address ? "[%s]:%d" : "%s:%d"),
+                localAddr.getHostAddress(), inetSockAddr.getPort());
+    }
+
+    private void prepareIcmpMeasurement(InetAddress target) {
+        if (!mIcmpChecks.containsKey(target)) {
+            Measurement measurement = new Measurement();
+            measurement.thread = new Thread(new IcmpCheck(target, measurement));
+            mIcmpChecks.put(target, measurement);
+        }
+    }
+
+    private void prepareExplicitSourceIcmpMeasurements(InetAddress target) {
+        for (LinkAddress l : mLinkProperties.getLinkAddresses()) {
+            InetAddress source = l.getAddress();
+            if (source instanceof Inet6Address && l.isGlobalPreferred()) {
+                Pair<InetAddress, InetAddress> srcTarget = new Pair<>(source, target);
+                if (!mExplicitSourceIcmpChecks.containsKey(srcTarget)) {
+                    Measurement measurement = new Measurement();
+                    measurement.thread = new Thread(new IcmpCheck(source, target, measurement));
+                    mExplicitSourceIcmpChecks.put(srcTarget, measurement);
+                }
+            }
+        }
+    }
+
+    private void prepareDnsMeasurement(InetAddress target) {
+        if (!mDnsUdpChecks.containsKey(target)) {
+            Measurement measurement = new Measurement();
+            measurement.thread = new Thread(new DnsUdpCheck(target, measurement));
+            mDnsUdpChecks.put(target, measurement);
+        }
+    }
+
+    private void prepareDnsTlsMeasurement(@Nullable String hostname, @NonNull InetAddress target) {
+        // This might overwrite an existing entry in mDnsTlsChecks, because |target| can be an IP
+        // address configured by the network as well as an IP address learned by resolving the
+        // strict mode DNS hostname. If the entry is overwritten, the overwritten measurement
+        // thread will not execute.
+        Measurement measurement = new Measurement();
+        measurement.thread = new Thread(new DnsTlsCheck(hostname, target, measurement));
+        mDnsTlsChecks.put(target, measurement);
+    }
+
+    private int totalMeasurementCount() {
+        return mIcmpChecks.size() + mExplicitSourceIcmpChecks.size() + mDnsUdpChecks.size()
+                + mDnsTlsChecks.size();
+    }
+
+    private void startMeasurements() {
+        for (Measurement measurement : mIcmpChecks.values()) {
+            measurement.thread.start();
+        }
+        for (Measurement measurement : mExplicitSourceIcmpChecks.values()) {
+            measurement.thread.start();
+        }
+        for (Measurement measurement : mDnsUdpChecks.values()) {
+            measurement.thread.start();
+        }
+        for (Measurement measurement : mDnsTlsChecks.values()) {
+            measurement.thread.start();
+        }
+    }
+
+    public void waitForMeasurements() {
+        try {
+            mCountDownLatch.await(mDeadlineTime - now(), TimeUnit.MILLISECONDS);
+        } catch (InterruptedException ignored) {}
+    }
+
+    public List<Measurement> getMeasurements() {
+        // TODO: Consider moving waitForMeasurements() in here to minimize the
+        // chance of caller errors.
+
+        ArrayList<Measurement> measurements = new ArrayList(totalMeasurementCount());
+
+        // Sort measurements IPv4 first.
+        for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) {
+            if (entry.getKey() instanceof Inet4Address) {
+                measurements.add(entry.getValue());
+            }
+        }
+        for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry :
+                mExplicitSourceIcmpChecks.entrySet()) {
+            if (entry.getKey().first instanceof Inet4Address) {
+                measurements.add(entry.getValue());
+            }
+        }
+        for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) {
+            if (entry.getKey() instanceof Inet4Address) {
+                measurements.add(entry.getValue());
+            }
+        }
+        for (Map.Entry<InetAddress, Measurement> entry : mDnsTlsChecks.entrySet()) {
+            if (entry.getKey() instanceof Inet4Address) {
+                measurements.add(entry.getValue());
+            }
+        }
+
+        // IPv6 measurements second.
+        for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) {
+            if (entry.getKey() instanceof Inet6Address) {
+                measurements.add(entry.getValue());
+            }
+        }
+        for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry :
+                mExplicitSourceIcmpChecks.entrySet()) {
+            if (entry.getKey().first instanceof Inet6Address) {
+                measurements.add(entry.getValue());
+            }
+        }
+        for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) {
+            if (entry.getKey() instanceof Inet6Address) {
+                measurements.add(entry.getValue());
+            }
+        }
+        for (Map.Entry<InetAddress, Measurement> entry : mDnsTlsChecks.entrySet()) {
+            if (entry.getKey() instanceof Inet6Address) {
+                measurements.add(entry.getValue());
+            }
+        }
+
+        return measurements;
+    }
+
+    public void dump(IndentingPrintWriter pw) {
+        pw.println(TAG + ":" + mDescription);
+        final long unfinished = mCountDownLatch.getCount();
+        if (unfinished > 0) {
+            // This can't happen unless a caller forgets to call waitForMeasurements()
+            // or a measurement isn't implemented to correctly honor the timeout.
+            pw.println("WARNING: countdown wait incomplete: "
+                    + unfinished + " unfinished measurements");
+        }
+
+        pw.increaseIndent();
+
+        String prefix;
+        for (Measurement m : getMeasurements()) {
+            prefix = m.checkSucceeded() ? "." : "F";
+            pw.println(prefix + "  " + m.toString());
+        }
+
+        pw.decreaseIndent();
+    }
+
+
+    private class SimpleSocketCheck implements Closeable {
+        protected final InetAddress mSource;  // Usually null.
+        protected final InetAddress mTarget;
+        protected final int mAddressFamily;
+        protected final Measurement mMeasurement;
+        protected FileDescriptor mFileDescriptor;
+        protected SocketAddress mSocketAddress;
+
+        protected SimpleSocketCheck(
+                InetAddress source, InetAddress target, Measurement measurement) {
+            mMeasurement = measurement;
+
+            if (target instanceof Inet6Address) {
+                Inet6Address targetWithScopeId = null;
+                if (target.isLinkLocalAddress() && mInterfaceIndex != null) {
+                    try {
+                        targetWithScopeId = Inet6Address.getByAddress(
+                                null, target.getAddress(), mInterfaceIndex);
+                    } catch (UnknownHostException e) {
+                        mMeasurement.recordFailure(e.toString());
+                    }
+                }
+                mTarget = (targetWithScopeId != null) ? targetWithScopeId : target;
+                mAddressFamily = AF_INET6;
+            } else {
+                mTarget = target;
+                mAddressFamily = AF_INET;
+            }
+
+            // We don't need to check the scope ID here because we currently only do explicit-source
+            // measurements from global IPv6 addresses.
+            mSource = source;
+        }
+
+        protected SimpleSocketCheck(InetAddress target, Measurement measurement) {
+            this(null, target, measurement);
+        }
+
+        protected void setupSocket(
+                int sockType, int protocol, long writeTimeout, long readTimeout, int dstPort)
+                throws ErrnoException, IOException {
+            final int oldTag = TrafficStats.getAndSetThreadStatsTag(
+                    NetworkStackConstants.TAG_SYSTEM_PROBE);
+            try {
+                mFileDescriptor = Os.socket(mAddressFamily, sockType, protocol);
+            } finally {
+                // TODO: The tag should remain set until all traffic is sent and received.
+                // Consider tagging the socket after the measurement thread is started.
+                TrafficStats.setThreadStatsTag(oldTag);
+            }
+            // Setting SNDTIMEO is purely for defensive purposes.
+            Os.setsockoptTimeval(mFileDescriptor,
+                    SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(writeTimeout));
+            Os.setsockoptTimeval(mFileDescriptor,
+                    SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(readTimeout));
+            // TODO: Use IP_RECVERR/IPV6_RECVERR, pending OsContants availability.
+            mNetwork.bindSocket(mFileDescriptor);
+            if (mSource != null) {
+                Os.bind(mFileDescriptor, mSource, 0);
+            }
+            Os.connect(mFileDescriptor, mTarget, dstPort);
+            mSocketAddress = Os.getsockname(mFileDescriptor);
+        }
+
+        protected boolean ensureMeasurementNecessary() {
+            if (mMeasurement.finishTime == 0) return false;
+
+            // Countdown latch was not decremented when the measurement failed during setup.
+            mCountDownLatch.countDown();
+            return true;
+        }
+
+        @Override
+        public void close() {
+            IoUtils.closeQuietly(mFileDescriptor);
+        }
+    }
+
+
+    private class IcmpCheck extends SimpleSocketCheck implements Runnable {
+        private static final int TIMEOUT_SEND = 100;
+        private static final int TIMEOUT_RECV = 300;
+        private static final int PACKET_BUFSIZE = 512;
+        private final int mProtocol;
+        private final int mIcmpType;
+
+        public IcmpCheck(InetAddress source, InetAddress target, Measurement measurement) {
+            super(source, target, measurement);
+
+            if (mAddressFamily == AF_INET6) {
+                mProtocol = IPPROTO_ICMPV6;
+                mIcmpType = NetworkConstants.ICMPV6_ECHO_REQUEST_TYPE;
+                mMeasurement.description = "ICMPv6";
+            } else {
+                mProtocol = IPPROTO_ICMP;
+                mIcmpType = NetworkConstants.ICMPV4_ECHO_REQUEST_TYPE;
+                mMeasurement.description = "ICMPv4";
+            }
+
+            mMeasurement.description += " dst{" + mTarget.getHostAddress() + "}";
+        }
+
+        public IcmpCheck(InetAddress target, Measurement measurement) {
+            this(null, target, measurement);
+        }
+
+        @Override
+        public void run() {
+            if (ensureMeasurementNecessary()) return;
+
+            try {
+                setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0);
+            } catch (ErrnoException | IOException e) {
+                mMeasurement.recordFailure(e.toString());
+                return;
+            }
+            mMeasurement.description += " src{" + socketAddressToString(mSocketAddress) + "}";
+
+            // Build a trivial ICMP packet.
+            final byte[] icmpPacket = {
+                    (byte) mIcmpType, 0, 0, 0, 0, 0, 0, 0  // ICMP header
+            };
+
+            int count = 0;
+            mMeasurement.startTime = now();
+            while (now() < mDeadlineTime - (TIMEOUT_SEND + TIMEOUT_RECV)) {
+                count++;
+                icmpPacket[icmpPacket.length - 1] = (byte) count;
+                try {
+                    Os.write(mFileDescriptor, icmpPacket, 0, icmpPacket.length);
+                } catch (ErrnoException | InterruptedIOException e) {
+                    mMeasurement.recordFailure(e.toString());
+                    break;
+                }
+
+                try {
+                    ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE);
+                    Os.read(mFileDescriptor, reply);
+                    // TODO: send a few pings back to back to guesstimate packet loss.
+                    mMeasurement.recordSuccess("1/" + count);
+                    break;
+                } catch (ErrnoException | InterruptedIOException e) {
+                    continue;
+                }
+            }
+            if (mMeasurement.finishTime == 0) {
+                mMeasurement.recordFailure("0/" + count);
+            }
+
+            close();
+        }
+    }
+
+
+    private class DnsUdpCheck extends SimpleSocketCheck implements Runnable {
+        private static final int TIMEOUT_SEND = 100;
+        private static final int TIMEOUT_RECV = 500;
+        private static final int RR_TYPE_A = 1;
+        private static final int RR_TYPE_AAAA = 28;
+        private static final int PACKET_BUFSIZE = 512;
+
+        protected final Random mRandom = new Random();
+
+        // Should be static, but the compiler mocks our puny, human attempts at reason.
+        protected String responseCodeStr(int rcode) {
+            try {
+                return DnsResponseCode.values()[rcode].toString();
+            } catch (IndexOutOfBoundsException e) {
+                return String.valueOf(rcode);
+            }
+        }
+
+        protected final int mQueryType;
+
+        public DnsUdpCheck(InetAddress target, Measurement measurement) {
+            super(target, measurement);
+
+            // TODO: Ideally, query the target for both types regardless of address family.
+            if (mAddressFamily == AF_INET6) {
+                mQueryType = RR_TYPE_AAAA;
+            } else {
+                mQueryType = RR_TYPE_A;
+            }
+
+            mMeasurement.description = "DNS UDP dst{" + mTarget.getHostAddress() + "}";
+        }
+
+        @Override
+        public void run() {
+            if (ensureMeasurementNecessary()) return;
+
+            try {
+                setupSocket(SOCK_DGRAM, IPPROTO_UDP, TIMEOUT_SEND, TIMEOUT_RECV,
+                        NetworkConstants.DNS_SERVER_PORT);
+            } catch (ErrnoException | IOException e) {
+                mMeasurement.recordFailure(e.toString());
+                return;
+            }
+
+            // This needs to be fixed length so it can be dropped into the pre-canned packet.
+            final String sixRandomDigits = String.valueOf(mRandom.nextInt(900000) + 100000);
+            appendDnsToMeasurementDescription(sixRandomDigits, mSocketAddress);
+
+            // Build a trivial DNS packet.
+            final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits);
+
+            int count = 0;
+            mMeasurement.startTime = now();
+            while (now() < mDeadlineTime - (TIMEOUT_RECV + TIMEOUT_RECV)) {
+                count++;
+                try {
+                    Os.write(mFileDescriptor, dnsPacket, 0, dnsPacket.length);
+                } catch (ErrnoException | InterruptedIOException e) {
+                    mMeasurement.recordFailure(e.toString());
+                    break;
+                }
+
+                try {
+                    ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE);
+                    Os.read(mFileDescriptor, reply);
+                    // TODO: more correct and detailed evaluation of the response,
+                    // possibly adding the returned IP address(es) to the output.
+                    final String rcodeStr = (reply.limit() > 3)
+                            ? " " + responseCodeStr((int) (reply.get(3)) & 0x0f)
+                            : "";
+                    mMeasurement.recordSuccess("1/" + count + rcodeStr);
+                    break;
+                } catch (ErrnoException | InterruptedIOException e) {
+                    continue;
+                }
+            }
+            if (mMeasurement.finishTime == 0) {
+                mMeasurement.recordFailure("0/" + count);
+            }
+
+            close();
+        }
+
+        protected byte[] getDnsQueryPacket(String sixRandomDigits) {
+            byte[] rnd = sixRandomDigits.getBytes(StandardCharsets.US_ASCII);
+            return new byte[] {
+                (byte) mRandom.nextInt(), (byte) mRandom.nextInt(),  // [0-1]   query ID
+                1, 0,  // [2-3]   flags; byte[2] = 1 for recursion desired (RD).
+                0, 1,  // [4-5]   QDCOUNT (number of queries)
+                0, 0,  // [6-7]   ANCOUNT (number of answers)
+                0, 0,  // [8-9]   NSCOUNT (number of name server records)
+                0, 0,  // [10-11] ARCOUNT (number of additional records)
+                17, rnd[0], rnd[1], rnd[2], rnd[3], rnd[4], rnd[5],
+                        '-', 'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'd', 's',
+                6, 'm', 'e', 't', 'r', 'i', 'c',
+                7, 'g', 's', 't', 'a', 't', 'i', 'c',
+                3, 'c', 'o', 'm',
+                0,  // null terminator of FQDN (root TLD)
+                0, (byte) mQueryType,  // QTYPE
+                0, 1  // QCLASS, set to 1 = IN (Internet)
+            };
+        }
+
+        protected void appendDnsToMeasurementDescription(
+                String sixRandomDigits, SocketAddress sockAddr) {
+            mMeasurement.description += " src{" + socketAddressToString(sockAddr) + "}"
+                    + " qtype{" + mQueryType + "}"
+                    + " qname{" + sixRandomDigits + "-android-ds.metric.gstatic.com}";
+        }
+    }
+
+    // TODO: Have it inherited from SimpleSocketCheck, and separate common DNS helpers out of
+    // DnsUdpCheck.
+    private class DnsTlsCheck extends DnsUdpCheck {
+        private static final int TCP_CONNECT_TIMEOUT_MS = 2500;
+        private static final int TCP_TIMEOUT_MS = 2000;
+        private static final int DNS_TLS_PORT = 853;
+        private static final int DNS_HEADER_SIZE = 12;
+
+        private final String mHostname;
+
+        public DnsTlsCheck(@Nullable String hostname, @NonNull InetAddress target,
+                @NonNull Measurement measurement) {
+            super(target, measurement);
+
+            mHostname = hostname;
+            mMeasurement.description = "DNS TLS dst{" + mTarget.getHostAddress() + "} hostname{"
+                    + (mHostname == null ? "" : mHostname) + "}";
+        }
+
+        private SSLSocket setupSSLSocket() throws IOException {
+            // A TrustManager will be created and initialized with a KeyStore containing system
+            // CaCerts. During SSL handshake, it will be used to validate the certificates from
+            // the server.
+            SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+            sslSocket.setSoTimeout(TCP_TIMEOUT_MS);
+
+            if (!TextUtils.isEmpty(mHostname)) {
+                // Set SNI.
+                final List<SNIServerName> names =
+                        Collections.singletonList(new SNIHostName(mHostname));
+                SSLParameters params = sslSocket.getSSLParameters();
+                params.setServerNames(names);
+                sslSocket.setSSLParameters(params);
+            }
+
+            mNetwork.bindSocket(sslSocket);
+            return sslSocket;
+        }
+
+        private void sendDoTProbe(@Nullable SSLSocket sslSocket) throws IOException {
+            final String sixRandomDigits = String.valueOf(mRandom.nextInt(900000) + 100000);
+            final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits);
+
+            mMeasurement.startTime = now();
+            sslSocket.connect(new InetSocketAddress(mTarget, DNS_TLS_PORT), TCP_CONNECT_TIMEOUT_MS);
+
+            // Synchronous call waiting for the TLS handshake complete.
+            sslSocket.startHandshake();
+            appendDnsToMeasurementDescription(sixRandomDigits, sslSocket.getLocalSocketAddress());
+
+            final DataOutputStream output = new DataOutputStream(sslSocket.getOutputStream());
+            output.writeShort(dnsPacket.length);
+            output.write(dnsPacket, 0, dnsPacket.length);
+
+            final DataInputStream input = new DataInputStream(sslSocket.getInputStream());
+            final int replyLength = Short.toUnsignedInt(input.readShort());
+            final byte[] reply = new byte[replyLength];
+            int bytesRead = 0;
+            while (bytesRead < replyLength) {
+                bytesRead += input.read(reply, bytesRead, replyLength - bytesRead);
+            }
+
+            if (bytesRead > DNS_HEADER_SIZE && bytesRead == replyLength) {
+                mMeasurement.recordSuccess("1/1 " + responseCodeStr((int) (reply[3]) & 0x0f));
+            } else {
+                mMeasurement.recordFailure("1/1 Read " + bytesRead + " bytes while expected to be "
+                        + replyLength + " bytes");
+            }
+        }
+
+        @Override
+        public void run() {
+            if (ensureMeasurementNecessary()) return;
+
+            // No need to restore the tag, since this thread is only used for this measurement.
+            TrafficStats.getAndSetThreadStatsTag(NetworkStackConstants.TAG_SYSTEM_PROBE);
+
+            try (SSLSocket sslSocket = setupSSLSocket()) {
+                sendDoTProbe(sslSocket);
+            } catch (IOException e) {
+                mMeasurement.recordFailure(e.toString());
+            }
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
new file mode 100644
index 0000000..b57ad5d
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
@@ -0,0 +1,400 @@
+/*
+ * 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.connectivity;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.drawable.Icon;
+import android.net.ConnectivityResources;
+import android.net.NetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.wifi.WifiInfo;
+import android.os.UserHandle;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.widget.Toast;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+
+public class NetworkNotificationManager {
+
+
+    public static enum NotificationType {
+        LOST_INTERNET(SystemMessage.NOTE_NETWORK_LOST_INTERNET),
+        NETWORK_SWITCH(SystemMessage.NOTE_NETWORK_SWITCH),
+        NO_INTERNET(SystemMessage.NOTE_NETWORK_NO_INTERNET),
+        PARTIAL_CONNECTIVITY(SystemMessage.NOTE_NETWORK_PARTIAL_CONNECTIVITY),
+        SIGN_IN(SystemMessage.NOTE_NETWORK_SIGN_IN),
+        PRIVATE_DNS_BROKEN(SystemMessage.NOTE_NETWORK_PRIVATE_DNS_BROKEN);
+
+        public final int eventId;
+
+        NotificationType(int eventId) {
+            this.eventId = eventId;
+            Holder.sIdToTypeMap.put(eventId, this);
+        }
+
+        private static class Holder {
+            private static SparseArray<NotificationType> sIdToTypeMap = new SparseArray<>();
+        }
+
+        public static NotificationType getFromId(int id) {
+            return Holder.sIdToTypeMap.get(id);
+        }
+    };
+
+    private static final String TAG = NetworkNotificationManager.class.getSimpleName();
+    private static final boolean DBG = true;
+
+    // Notification channels used by ConnectivityService mainline module, it should be aligned with
+    // SystemNotificationChannels so the channels are the same as the ones used as the system
+    // server.
+    public static final String NOTIFICATION_CHANNEL_NETWORK_STATUS = "NETWORK_STATUS";
+    public static final String NOTIFICATION_CHANNEL_NETWORK_ALERTS = "NETWORK_ALERTS";
+
+    // The context is for the current user (system server)
+    private final Context mContext;
+    private final ConnectivityResources mResources;
+    private final TelephonyManager mTelephonyManager;
+    // The notification manager is created from a context for User.ALL, so notifications
+    // will be sent to all users.
+    private final NotificationManager mNotificationManager;
+    // Tracks the types of notifications managed by this instance, from creation to cancellation.
+    private final SparseIntArray mNotificationTypeMap;
+
+    public NetworkNotificationManager(@NonNull final Context c, @NonNull final TelephonyManager t) {
+        mContext = c;
+        mTelephonyManager = t;
+        mNotificationManager =
+                (NotificationManager) c.createContextAsUser(UserHandle.ALL, 0 /* flags */)
+                        .getSystemService(Context.NOTIFICATION_SERVICE);
+        mNotificationTypeMap = new SparseIntArray();
+        mResources = new ConnectivityResources(mContext);
+    }
+
+    @VisibleForTesting
+    protected static int approximateTransportType(NetworkAgentInfo nai) {
+        return nai.isVPN() ? TRANSPORT_VPN : getFirstTransportType(nai);
+    }
+
+    // TODO: deal more gracefully with multi-transport networks.
+    private static int getFirstTransportType(NetworkAgentInfo nai) {
+        // TODO: The range is wrong, the safer and correct way is to change the range from
+        // MIN_TRANSPORT to MAX_TRANSPORT.
+        for (int i = 0; i < 64; i++) {
+            if (nai.networkCapabilities.hasTransport(i)) return i;
+        }
+        return -1;
+    }
+
+    private String getTransportName(final int transportType) {
+        String[] networkTypes = mResources.get().getStringArray(R.array.network_switch_type_name);
+        try {
+            return networkTypes[transportType];
+        } catch (IndexOutOfBoundsException e) {
+            return mResources.get().getString(R.string.network_switch_type_name_unknown);
+        }
+    }
+
+    private static int getIcon(int transportType) {
+        return (transportType == TRANSPORT_WIFI)
+                ? R.drawable.stat_notify_wifi_in_range  // TODO: Distinguish ! from ?.
+                : R.drawable.stat_notify_rssi_in_range;
+    }
+
+    /**
+     * Show or hide network provisioning notifications.
+     *
+     * We use notifications for two purposes: to notify that a network requires sign in
+     * (NotificationType.SIGN_IN), or to notify that a network does not have Internet access
+     * (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a
+     * particular network we can display the notification type that was most recently requested.
+     * So for example if a captive portal fails to reply within a few seconds of connecting, we
+     * might first display NO_INTERNET, and then when the captive portal check completes, display
+     * SIGN_IN.
+     *
+     * @param id an identifier that uniquely identifies this notification.  This must match
+     *         between show and hide calls.  We use the NetID value but for legacy callers
+     *         we concatenate the range of types with the range of NetIDs.
+     * @param notifyType the type of the notification.
+     * @param nai the network with which the notification is associated. For a SIGN_IN, NO_INTERNET,
+     *         or LOST_INTERNET notification, this is the network we're connecting to. For a
+     *         NETWORK_SWITCH notification it's the network that we switched from. When this network
+     *         disconnects the notification is removed.
+     * @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null
+     *         in all other cases. Only used to determine the text of the notification.
+     */
+    public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
+            NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) {
+        final String tag = tagFor(id);
+        final int eventId = notifyType.eventId;
+        final int transportType;
+        final CharSequence name;
+        if (nai != null) {
+            transportType = approximateTransportType(nai);
+            final String extraInfo = nai.networkInfo.getExtraInfo();
+            if (nai.linkProperties != null && nai.linkProperties.getCaptivePortalData() != null
+                    && !TextUtils.isEmpty(nai.linkProperties.getCaptivePortalData()
+                    .getVenueFriendlyName())) {
+                name = nai.linkProperties.getCaptivePortalData().getVenueFriendlyName();
+            } else {
+                name = TextUtils.isEmpty(extraInfo)
+                        ? WifiInfo.sanitizeSsid(nai.networkCapabilities.getSsid()) : extraInfo;
+            }
+            // Only notify for Internet-capable networks.
+            if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return;
+        } else {
+            // Legacy notifications.
+            transportType = TRANSPORT_CELLULAR;
+            name = "";
+        }
+
+        // Clear any previous notification with lower priority, otherwise return. http://b/63676954.
+        // A new SIGN_IN notification with a new intent should override any existing one.
+        final int previousEventId = mNotificationTypeMap.get(id);
+        final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId);
+        if (priority(previousNotifyType) > priority(notifyType)) {
+            Log.d(TAG, String.format(
+                    "ignoring notification %s for network %s with existing notification %s",
+                    notifyType, id, previousNotifyType));
+            return;
+        }
+        clearNotification(id);
+
+        if (DBG) {
+            Log.d(TAG, String.format(
+                    "showNotification tag=%s event=%s transport=%s name=%s highPriority=%s",
+                    tag, nameOf(eventId), getTransportName(transportType), name, highPriority));
+        }
+
+        final Resources r = mResources.get();
+        final CharSequence title;
+        final CharSequence details;
+        Icon icon = Icon.createWithResource(
+                mResources.getResourcesContext(), getIcon(transportType));
+        if (notifyType == NotificationType.NO_INTERNET && transportType == TRANSPORT_WIFI) {
+            title = r.getString(R.string.wifi_no_internet, name);
+            details = r.getString(R.string.wifi_no_internet_detailed);
+        } else if (notifyType == NotificationType.PRIVATE_DNS_BROKEN) {
+            if (transportType == TRANSPORT_CELLULAR) {
+                title = r.getString(R.string.mobile_no_internet);
+            } else if (transportType == TRANSPORT_WIFI) {
+                title = r.getString(R.string.wifi_no_internet, name);
+            } else {
+                title = r.getString(R.string.other_networks_no_internet);
+            }
+            details = r.getString(R.string.private_dns_broken_detailed);
+        } else if (notifyType == NotificationType.PARTIAL_CONNECTIVITY
+                && transportType == TRANSPORT_WIFI) {
+            title = r.getString(R.string.network_partial_connectivity, name);
+            details = r.getString(R.string.network_partial_connectivity_detailed);
+        } else if (notifyType == NotificationType.LOST_INTERNET &&
+                transportType == TRANSPORT_WIFI) {
+            title = r.getString(R.string.wifi_no_internet, name);
+            details = r.getString(R.string.wifi_no_internet_detailed);
+        } else if (notifyType == NotificationType.SIGN_IN) {
+            switch (transportType) {
+                case TRANSPORT_WIFI:
+                    title = r.getString(R.string.wifi_available_sign_in, 0);
+                    details = r.getString(R.string.network_available_sign_in_detailed, name);
+                    break;
+                case TRANSPORT_CELLULAR:
+                    title = r.getString(R.string.network_available_sign_in, 0);
+                    // TODO: Change this to pull from NetworkInfo once a printable
+                    // name has been added to it
+                    NetworkSpecifier specifier = nai.networkCapabilities.getNetworkSpecifier();
+                    int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
+                    if (specifier instanceof TelephonyNetworkSpecifier) {
+                        subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
+                    }
+
+                    details = mTelephonyManager.createForSubscriptionId(subId)
+                            .getNetworkOperatorName();
+                    break;
+                default:
+                    title = r.getString(R.string.network_available_sign_in, 0);
+                    details = r.getString(R.string.network_available_sign_in_detailed, name);
+                    break;
+            }
+        } else if (notifyType == NotificationType.NETWORK_SWITCH) {
+            String fromTransport = getTransportName(transportType);
+            String toTransport = getTransportName(approximateTransportType(switchToNai));
+            title = r.getString(R.string.network_switch_metered, toTransport);
+            details = r.getString(R.string.network_switch_metered_detail, toTransport,
+                    fromTransport);
+        } else if (notifyType == NotificationType.NO_INTERNET
+                    || notifyType == NotificationType.PARTIAL_CONNECTIVITY) {
+            // NO_INTERNET and PARTIAL_CONNECTIVITY notification for non-WiFi networks
+            // are sent, but they are not implemented yet.
+            return;
+        } else {
+            Log.wtf(TAG, "Unknown notification type " + notifyType + " on network transport "
+                    + getTransportName(transportType));
+            return;
+        }
+        // When replacing an existing notification for a given network, don't alert, just silently
+        // update the existing notification. Note that setOnlyAlertOnce() will only work for the
+        // same id, and the id used here is the NotificationType which is different in every type of
+        // notification. This is required because the notification metrics only track the ID but not
+        // the tag.
+        final boolean hasPreviousNotification = previousNotifyType != null;
+        final String channelId = (highPriority && !hasPreviousNotification)
+                ? NOTIFICATION_CHANNEL_NETWORK_ALERTS : NOTIFICATION_CHANNEL_NETWORK_STATUS;
+        Notification.Builder builder = new Notification.Builder(mContext, channelId)
+                .setWhen(System.currentTimeMillis())
+                .setShowWhen(notifyType == NotificationType.NETWORK_SWITCH)
+                .setSmallIcon(icon)
+                .setAutoCancel(true)
+                .setTicker(title)
+                .setColor(mContext.getColor(android.R.color.system_notification_accent_color))
+                .setContentTitle(title)
+                .setContentIntent(intent)
+                .setLocalOnly(true)
+                .setOnlyAlertOnce(true);
+
+        if (notifyType == NotificationType.NETWORK_SWITCH) {
+            builder.setStyle(new Notification.BigTextStyle().bigText(details));
+        } else {
+            builder.setContentText(details);
+        }
+
+        if (notifyType == NotificationType.SIGN_IN) {
+            builder.extend(new Notification.TvExtender().setChannelId(channelId));
+        }
+
+        Notification notification = builder.build();
+
+        mNotificationTypeMap.put(id, eventId);
+        try {
+            mNotificationManager.notify(tag, eventId, notification);
+        } catch (NullPointerException npe) {
+            Log.d(TAG, "setNotificationVisible: visible notificationManager error", npe);
+        }
+    }
+
+    /**
+     * Clear the notification with the given id, only if it matches the given type.
+     */
+    public void clearNotification(int id, NotificationType notifyType) {
+        final int previousEventId = mNotificationTypeMap.get(id);
+        final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId);
+        if (notifyType != previousNotifyType) {
+            return;
+        }
+        clearNotification(id);
+    }
+
+    public void clearNotification(int id) {
+        if (mNotificationTypeMap.indexOfKey(id) < 0) {
+            return;
+        }
+        final String tag = tagFor(id);
+        final int eventId = mNotificationTypeMap.get(id);
+        if (DBG) {
+            Log.d(TAG, String.format("clearing notification tag=%s event=%s", tag,
+                   nameOf(eventId)));
+        }
+        try {
+            mNotificationManager.cancel(tag, eventId);
+        } catch (NullPointerException npe) {
+            Log.d(TAG, String.format(
+                    "failed to clear notification tag=%s event=%s", tag, nameOf(eventId)), npe);
+        }
+        mNotificationTypeMap.delete(id);
+    }
+
+    /**
+     * Legacy provisioning notifications coming directly from DcTracker.
+     */
+    public void setProvNotificationVisible(boolean visible, int id, String action) {
+        if (visible) {
+            // For legacy purposes, action is sent as the action + the phone ID from DcTracker.
+            // Split the string here and send the phone ID as an extra instead.
+            String[] splitAction = action.split(":");
+            Intent intent = new Intent(splitAction[0]);
+            try {
+                intent.putExtra("provision.phone.id", Integer.parseInt(splitAction[1]));
+            } catch (NumberFormatException ignored) { }
+            PendingIntent pendingIntent = PendingIntent.getBroadcast(
+                    mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE);
+            showNotification(id, NotificationType.SIGN_IN, null, null, pendingIntent, false);
+        } else {
+            clearNotification(id);
+        }
+    }
+
+    public void showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+        String fromTransport = getTransportName(approximateTransportType(fromNai));
+        String toTransport = getTransportName(approximateTransportType(toNai));
+        String text = mResources.get().getString(
+                R.string.network_switch_metered_toast, fromTransport, toTransport);
+        Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
+    }
+
+    @VisibleForTesting
+    static String tagFor(int id) {
+        return String.format("ConnectivityNotification:%d", id);
+    }
+
+    @VisibleForTesting
+    static String nameOf(int eventId) {
+        NotificationType t = NotificationType.getFromId(eventId);
+        return (t != null) ? t.name() : "UNKNOWN";
+    }
+
+    /**
+     * A notification with a higher number will take priority over a notification with a lower
+     * number.
+     */
+    private static int priority(NotificationType t) {
+        if (t == null) {
+            return 0;
+        }
+        switch (t) {
+            case SIGN_IN:
+                return 6;
+            case PARTIAL_CONNECTIVITY:
+                return 5;
+            case PRIVATE_DNS_BROKEN:
+                return 4;
+            case NO_INTERNET:
+                return 3;
+            case NETWORK_SWITCH:
+                return 2;
+            case LOST_INTERNET:
+                return 1;
+            default:
+                return 0;
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
new file mode 100644
index 0000000..8285e7a
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -0,0 +1,148 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.net.INetworkOfferCallback;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.RemoteException;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents an offer made by a NetworkProvider to create a network if a need arises.
+ *
+ * This class contains the prospective score and capabilities of the network. The provider
+ * is not obligated to caps able to create a network satisfying this, nor to build a network
+ * with the exact score and/or capabilities passed ; after all, not all providers know in
+ * advance what a network will look like after it's connected. Instead, this is meant as a
+ * filter to limit requests sent to the provider by connectivity to those that this offer stands
+ * a chance to fulfill.
+ *
+ * @see NetworkProvider#offerNetwork.
+ *
+ * @hide
+ */
+public class NetworkOffer implements NetworkRanker.Scoreable {
+    @NonNull public final FullScore score;
+    @NonNull public final NetworkCapabilities caps;
+    @NonNull public final INetworkOfferCallback callback;
+    @NonNull public final int providerId;
+    // While this could, in principle, be deduced from the old values of the satisfying networks,
+    // doing so would add a lot of complexity and performance penalties. For each request, the
+    // ranker would have to run again to figure out if this offer used to be able to beat the
+    // previous satisfier to know if there is a change in whether this offer is now needed ;
+    // besides, there would be a need to handle an edge case when a new request comes online,
+    // where it's not satisfied before the first rematch, where starting to satisfy a request
+    // should not result in sending unneeded to this offer. This boolean, while requiring that
+    // the offers are only ever manipulated on the CS thread, is by far a simpler and
+    // economical solution.
+    private final Set<NetworkRequest> mCurrentlyNeeded = new HashSet<>();
+
+    public NetworkOffer(@NonNull final FullScore score,
+            @NonNull final NetworkCapabilities caps,
+            @NonNull final INetworkOfferCallback callback,
+            @NonNull final int providerId) {
+        this.score = Objects.requireNonNull(score);
+        this.caps = Objects.requireNonNull(caps);
+        this.callback = Objects.requireNonNull(callback);
+        this.providerId = providerId;
+    }
+
+    /**
+     * Get the score filter of this offer
+     */
+    @Override @NonNull public FullScore getScore() {
+        return score;
+    }
+
+    /**
+     * Get the capabilities filter of this offer
+     */
+    @Override @NonNull public NetworkCapabilities getCapsNoCopy() {
+        return caps;
+    }
+
+    /**
+     * Tell the provider for this offer that the network is needed for a request.
+     * @param request the request for which the offer is needed
+     */
+    public void onNetworkNeeded(@NonNull final NetworkRequest request) {
+        if (mCurrentlyNeeded.contains(request)) {
+            throw new IllegalStateException("Network already needed");
+        }
+        mCurrentlyNeeded.add(request);
+        try {
+            callback.onNetworkNeeded(request);
+        } catch (final RemoteException e) {
+            // The provider is dead. It will be removed by the death recipient.
+        }
+    }
+
+    /**
+     * Tell the provider for this offer that the network is no longer needed for this request.
+     *
+     * onNetworkNeeded will have been called with the same request before.
+     *
+     * @param request the request
+     */
+    public void onNetworkUnneeded(@NonNull final NetworkRequest request) {
+        if (!mCurrentlyNeeded.contains(request)) {
+            throw new IllegalStateException("Network already unneeded");
+        }
+        mCurrentlyNeeded.remove(request);
+        try {
+            callback.onNetworkUnneeded(request);
+        } catch (final RemoteException e) {
+            // The provider is dead. It will be removed by the death recipient.
+        }
+    }
+
+    /**
+     * Returns whether this offer is currently needed for this request.
+     * @param request the request
+     * @return whether the offer is currently considered needed
+     */
+    public boolean neededFor(@NonNull final NetworkRequest request) {
+        return mCurrentlyNeeded.contains(request);
+    }
+
+    /**
+     * Migrate from, and take over, a previous offer.
+     *
+     * When an updated offer is sent from a provider, call this method on the new offer, passing
+     * the old one, to take over the state.
+     *
+     * @param previousOffer the previous offer
+     */
+    public void migrateFrom(@NonNull final NetworkOffer previousOffer) {
+        if (!callback.asBinder().equals(previousOffer.callback.asBinder())) {
+            throw new IllegalArgumentException("Can only migrate from a previous version of"
+                    + " the same offer");
+        }
+        mCurrentlyNeeded.clear();
+        mCurrentlyNeeded.addAll(previousOffer.mCurrentlyNeeded);
+    }
+
+    @Override
+    public String toString() {
+        return "NetworkOffer [ Score " + score + " ]";
+    }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkRanker.java b/service/src/com/android/server/connectivity/NetworkRanker.java
new file mode 100644
index 0000000..e839837
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkRanker.java
@@ -0,0 +1,299 @@
+/*
+ * 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.connectivity;
+
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkScore.POLICY_EXITING;
+import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY;
+import static android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI;
+
+import static com.android.net.module.util.CollectionUtils.filter;
+import static com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED;
+import static com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED;
+import static com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD;
+import static com.android.server.connectivity.FullScore.POLICY_IS_INVINCIBLE;
+import static com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED;
+import static com.android.server.connectivity.FullScore.POLICY_IS_VPN;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+
+import com.android.net.module.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * A class that knows how to find the best network matching a request out of a list of networks.
+ */
+public class NetworkRanker {
+    // Historically the legacy ints have been 0~100 in principle (though the highest score in
+    // AOSP has always been 90). This is relied on by VPNs that send a legacy score of 101.
+    public static final int LEGACY_INT_MAX = 100;
+
+    /**
+     * A class that can be scored against other scoreables.
+     */
+    public interface Scoreable {
+        /** Get score of this scoreable */
+        FullScore getScore();
+        /** Get capabilities of this scoreable */
+        NetworkCapabilities getCapsNoCopy();
+    }
+
+    private static final boolean USE_POLICY_RANKING = true;
+
+    public NetworkRanker() { }
+
+    /**
+     * Find the best network satisfying this request among the list of passed networks.
+     */
+    @Nullable
+    public NetworkAgentInfo getBestNetwork(@NonNull final NetworkRequest request,
+            @NonNull final Collection<NetworkAgentInfo> nais,
+            @Nullable final NetworkAgentInfo currentSatisfier) {
+        final ArrayList<NetworkAgentInfo> candidates = filter(nais, nai -> nai.satisfies(request));
+        if (candidates.size() == 1) return candidates.get(0); // Only one potential satisfier
+        if (candidates.size() <= 0) return null; // No network can satisfy this request
+        if (USE_POLICY_RANKING) {
+            return getBestNetworkByPolicy(candidates, currentSatisfier);
+        } else {
+            return getBestNetworkByLegacyInt(candidates);
+        }
+    }
+
+    // Transport preference order, if it comes down to that.
+    private static final int[] PREFERRED_TRANSPORTS_ORDER = { TRANSPORT_ETHERNET, TRANSPORT_WIFI,
+            TRANSPORT_BLUETOOTH, TRANSPORT_CELLULAR };
+
+    // Function used to partition a list into two working areas depending on whether they
+    // satisfy a predicate. All items satisfying the predicate will be put in |positive|, all
+    // items that don't will be put in |negative|.
+    // This is useful in this file because many of the ranking checks will retain only networks that
+    // satisfy a predicate if any of them do, but keep them all if all of them do. Having working
+    // areas is uncustomary in Java, but this function is called in a fairly intensive manner
+    // and doing allocation quite that often might affect performance quite badly.
+    private static <T> void partitionInto(@NonNull final List<T> source, @NonNull Predicate<T> test,
+            @NonNull final List<T> positive, @NonNull final List<T> negative) {
+        positive.clear();
+        negative.clear();
+        for (final T item : source) {
+            if (test.test(item)) {
+                positive.add(item);
+            } else {
+                negative.add(item);
+            }
+        }
+    }
+
+    @Nullable private <T extends Scoreable> T getBestNetworkByPolicy(
+            @NonNull List<T> candidates,
+            @Nullable final T currentSatisfier) {
+        // Used as working areas.
+        final ArrayList<T> accepted =
+                new ArrayList<>(candidates.size() /* initialCapacity */);
+        final ArrayList<T> rejected =
+                new ArrayList<>(candidates.size() /* initialCapacity */);
+
+        // The following tests will search for a network matching a given criterion. They all
+        // function the same way : if any network matches the criterion, drop from consideration
+        // all networks that don't. To achieve this, the tests below :
+        // 1. partition the list of remaining candidates into accepted and rejected networks.
+        // 2. if only one candidate remains, that's the winner : if accepted.size == 1 return [0]
+        // 3. if multiple remain, keep only the accepted networks and go on to the next criterion.
+        //    Because the working areas will be wiped, a copy of the accepted networks needs to be
+        //    made.
+        // 4. if none remain, the criterion did not help discriminate so keep them all. As an
+        //    optimization, skip creating a new array and go on to the next criterion.
+
+        // If a network is invincible, use it.
+        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_INVINCIBLE),
+                accepted, rejected);
+        if (accepted.size() == 1) return accepted.get(0);
+        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
+        // If there is a connected VPN, use it.
+        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VPN),
+                accepted, rejected);
+        if (accepted.size() == 1) return accepted.get(0);
+        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
+        // Selected & Accept-unvalidated policy : if any network has both of these, then don't
+        // choose one that doesn't.
+        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_EVER_USER_SELECTED)
+                        && nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED),
+                accepted, rejected);
+        if (accepted.size() == 1) return accepted.get(0);
+        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
+        // Yield to bad wifi policy : if any wifi has ever been validated (even if it's now
+        // unvalidated), and unless it's been explicitly avoided when bad in UI, then keep only
+        // networks that don't yield to such a wifi network.
+        final boolean anyWiFiEverValidated = CollectionUtils.any(candidates,
+                nai -> nai.getScore().hasPolicy(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD)
+                        && nai.getCapsNoCopy().hasTransport(TRANSPORT_WIFI));
+        if (anyWiFiEverValidated) {
+            partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI),
+                    accepted, rejected);
+            if (accepted.size() == 1) return accepted.get(0);
+            if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+        }
+
+        // If any network is validated (or should be accepted even if it's not validated), then
+        // don't choose one that isn't.
+        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VALIDATED)
+                        || nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED),
+                accepted, rejected);
+        if (accepted.size() == 1) return accepted.get(0);
+        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
+        // If any network is not exiting, don't choose one that is.
+        partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_EXITING),
+                accepted, rejected);
+        if (accepted.size() == 1) return accepted.get(0);
+        if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
+        // TODO : If any network is unmetered, don't choose a metered network.
+        // This can't be implemented immediately because prospective networks are always
+        // considered unmetered because factories don't know if the network will be metered.
+        // Saying an unmetered network always beats a metered one would mean that when metered wifi
+        // is connected, the offer for telephony would beat WiFi but the actual metered network
+        // would lose, so we'd have an infinite loop where telephony would continually bring up
+        // a network that is immediately torn down.
+        // Fix this by getting the agent to tell connectivity whether the network they will
+        // bring up is metered. Cell knows that in advance, while WiFi has a good estimate and
+        // can revise it if the network later turns out to be metered.
+        // partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_UNMETERED),
+        //         accepted, rejected);
+        // if (accepted.size() == 1) return accepted.get(0);
+        // if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
+
+        // If any network is for the default subscription, don't choose a network for another
+        // subscription with the same transport.
+        partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY),
+                accepted, rejected);
+        for (final Scoreable defaultSubNai : accepted) {
+            // Remove all networks without the DEFAULT_SUBSCRIPTION policy and the same transports
+            // as a network that has it.
+            final int[] transports = defaultSubNai.getCapsNoCopy().getTransportTypes();
+            candidates.removeIf(nai -> !nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY)
+                    && Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes()));
+        }
+        if (1 == candidates.size()) return candidates.get(0);
+        // It's guaranteed candidates.size() > 0 because there is at least one with the
+        // TRANSPORT_PRIMARY policy and only those without it were removed.
+
+        // If some of the networks have a better transport than others, keep only the ones with
+        // the best transports.
+        for (final int transport : PREFERRED_TRANSPORTS_ORDER) {
+            partitionInto(candidates, nai -> nai.getCapsNoCopy().hasTransport(transport),
+                    accepted, rejected);
+            if (accepted.size() == 1) return accepted.get(0);
+            if (accepted.size() > 0 && rejected.size() > 0) {
+                candidates = new ArrayList<>(accepted);
+                break;
+            }
+        }
+
+        // At this point there are still multiple networks passing all the tests above. If any
+        // of them is the previous satisfier, keep it.
+        if (candidates.contains(currentSatisfier)) return currentSatisfier;
+
+        // If there are still multiple options at this point but none of them is any of the
+        // transports above, it doesn't matter which is returned. They are all the same.
+        return candidates.get(0);
+    }
+
+    // TODO : switch to the policy implementation and remove
+    // Almost equivalent to Collections.max(nais), but allows returning null if no network
+    // satisfies the request.
+    private NetworkAgentInfo getBestNetworkByLegacyInt(
+            @NonNull final Collection<NetworkAgentInfo> nais) {
+        NetworkAgentInfo bestNetwork = null;
+        int bestScore = Integer.MIN_VALUE;
+        for (final NetworkAgentInfo nai : nais) {
+            final int naiScore = nai.getCurrentScore();
+            if (naiScore > bestScore) {
+                bestNetwork = nai;
+                bestScore = naiScore;
+            }
+        }
+        return bestNetwork;
+    }
+
+    /**
+     * Returns whether a {@link Scoreable} has a chance to beat a champion network for a request.
+     *
+     * Offers are sent by network providers when they think they might be able to make a network
+     * with the characteristics contained in the offer. If the offer has no chance to beat
+     * the currently best network for a given request, there is no point in the provider spending
+     * power trying to find and bring up such a network.
+     *
+     * Note that having an offer up does not constitute a commitment from the provider part
+     * to be able to bring up a network with these characteristics, or a network at all for
+     * that matter. This is only used to save power by letting providers know when they can't
+     * beat a current champion.
+     *
+     * @param request The request to evaluate against.
+     * @param champion The currently best network for this request.
+     * @param contestant The offer.
+     * @return Whether the offer stands a chance to beat the champion.
+     */
+    public boolean mightBeat(@NonNull final NetworkRequest request,
+            @Nullable final NetworkAgentInfo champion,
+            @NonNull final Scoreable contestant) {
+        // If this network can't even satisfy the request then it can't beat anything, not
+        // even an absence of network. It can't satisfy it anyway.
+        if (!request.canBeSatisfiedBy(contestant.getCapsNoCopy())) return false;
+        // If there is no satisfying network, then this network can beat, because some network
+        // is always better than no network.
+        if (null == champion) return true;
+        if (USE_POLICY_RANKING) {
+            // If there is no champion, the offer can always beat.
+            // Otherwise rank them.
+            final ArrayList<Scoreable> candidates = new ArrayList<>();
+            candidates.add(champion);
+            candidates.add(contestant);
+            return contestant == getBestNetworkByPolicy(candidates, champion);
+        } else {
+            return mightBeatByLegacyInt(champion.getScore(), contestant);
+        }
+    }
+
+    /**
+     * Returns whether a contestant might beat a champion according to the legacy int.
+     */
+    private boolean mightBeatByLegacyInt(@Nullable final FullScore championScore,
+            @NonNull final Scoreable contestant) {
+        final int offerIntScore;
+        if (contestant.getCapsNoCopy().hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+            // If the offer might have Internet access, then it might validate.
+            offerIntScore = contestant.getScore().getLegacyIntAsValidated();
+        } else {
+            offerIntScore = contestant.getScore().getLegacyInt();
+        }
+        return championScore.getLegacyInt() < offerIntScore;
+    }
+}
diff --git a/service/src/com/android/server/connectivity/OsCompat.java b/service/src/com/android/server/connectivity/OsCompat.java
new file mode 100644
index 0000000..57e3dcd
--- /dev/null
+++ b/service/src/com/android/server/connectivity/OsCompat.java
@@ -0,0 +1,75 @@
+/*
+ * 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 android.system.ErrnoException;
+import android.system.Os;
+
+import java.io.FileDescriptor;
+
+/**
+ * Compatibility utility for android.system.Os core platform APIs.
+ *
+ * Connectivity has access to such APIs, but they are not part of the module_current stubs yet
+ * (only core_current). Most stable core platform APIs are included manually in the connectivity
+ * build rules, but because Os is also part of the base java SDK that is earlier on the
+ * classpath, the extra core platform APIs are not seen.
+ *
+ * TODO (b/157639992, b/183097033): remove as soon as core_current is part of system_server_current
+ * @hide
+ */
+public class OsCompat {
+    // This value should be correct on all architectures supported by Android, but hardcoding ioctl
+    // numbers should be avoided.
+    /**
+     * @see android.system.OsConstants#TIOCOUTQ
+     */
+    public static final int TIOCOUTQ = 0x5411;
+
+    /**
+     * @see android.system.Os#getsockoptInt(FileDescriptor, int, int)
+     */
+    public static int getsockoptInt(FileDescriptor fd, int level, int option) throws
+            ErrnoException {
+        try {
+            return (int) Os.class.getMethod(
+                    "getsockoptInt", FileDescriptor.class, int.class, int.class)
+                    .invoke(null, fd, level, option);
+        } catch (ReflectiveOperationException e) {
+            if (e.getCause() instanceof ErrnoException) {
+                throw (ErrnoException) e.getCause();
+            }
+            throw new IllegalStateException("Error calling getsockoptInt", e);
+        }
+    }
+
+    /**
+     * @see android.system.Os#ioctlInt(FileDescriptor, int)
+     */
+    public static int ioctlInt(FileDescriptor fd, int cmd) throws
+            ErrnoException {
+        try {
+            return (int) Os.class.getMethod(
+                    "ioctlInt", FileDescriptor.class, int.class).invoke(null, fd, cmd);
+        } catch (ReflectiveOperationException e) {
+            if (e.getCause() instanceof ErrnoException) {
+                throw (ErrnoException) e.getCause();
+            }
+            throw new IllegalStateException("Error calling ioctlInt", e);
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
new file mode 100644
index 0000000..32e06e5
--- /dev/null
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -0,0 +1,837 @@
+/*
+ * 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.connectivity;
+
+import static android.Manifest.permission.CHANGE_NETWORK_STATE;
+import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
+import static android.Manifest.permission.INTERNET;
+import static android.Manifest.permission.NETWORK_STACK;
+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.content.pm.PackageManager.MATCH_ANY_USER;
+import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.os.Process.INVALID_UID;
+import static android.os.Process.SYSTEM_UID;
+
+import static com.android.net.module.util.CollectionUtils.toIntArray;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.ContentObserver;
+import android.net.ConnectivitySettingsManager;
+import android.net.INetd;
+import android.net.UidRange;
+import android.net.Uri;
+import android.os.Build;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.SystemConfigManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.system.OsConstants;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * A utility class to inform Netd of UID permisisons.
+ * Does a mass update at boot and then monitors for app install/remove.
+ *
+ * @hide
+ */
+public class PermissionMonitor {
+    private static final String TAG = "PermissionMonitor";
+    private static final boolean DBG = true;
+    protected static final Boolean SYSTEM = Boolean.TRUE;
+    protected static final Boolean NETWORK = Boolean.FALSE;
+    private static final int VERSION_Q = Build.VERSION_CODES.Q;
+
+    private final PackageManager mPackageManager;
+    private final UserManager mUserManager;
+    private final SystemConfigManager mSystemConfigManager;
+    private final INetd mNetd;
+    private final Dependencies mDeps;
+    private final Context mContext;
+
+    @GuardedBy("this")
+    private final Set<UserHandle> mUsers = new HashSet<>();
+
+    // Keys are app uids. Values are true for SYSTEM permission and false for NETWORK permission.
+    @GuardedBy("this")
+    private final Map<Integer, Boolean> mApps = new HashMap<>();
+
+    // Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges
+    // for apps under the VPN
+    @GuardedBy("this")
+    private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>();
+
+    // 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
+    // added/removed.
+    @GuardedBy("this")
+    private final Set<Integer> mAllApps = new HashSet<>();
+
+    // A set of uids which are allowed to use restricted networks. The packages of these uids can't
+    // hold the CONNECTIVITY_USE_RESTRICTED_NETWORKS permission because they can't be
+    // signature|privileged apps. However, these apps should still be able to use restricted
+    // networks under certain conditions (e.g. government app using emergency services). So grant
+    // netd system permission to these uids which is listed in UIDS_ALLOWED_ON_RESTRICTED_NETWORKS.
+    @GuardedBy("this")
+    private final Set<Integer> mUidsAllowedOnRestrictedNetworks = new ArraySet<>();
+
+    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+            final Uri packageData = intent.getData();
+            final String packageName =
+                    packageData != null ? packageData.getSchemeSpecificPart() : null;
+
+            if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+                onPackageAdded(packageName, uid);
+            } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+                onPackageRemoved(packageName, uid);
+            } else {
+                Log.wtf(TAG, "received unexpected intent: " + action);
+            }
+        }
+    };
+
+    /**
+     * Dependencies of PermissionMonitor, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * Get device first sdk version.
+         */
+        public int getDeviceFirstSdkInt() {
+            return Build.VERSION.FIRST_SDK_INT;
+        }
+
+        /**
+         * Get uids allowed to use restricted networks via ConnectivitySettingsManager.
+         */
+        public Set<Integer> getUidsAllowedOnRestrictedNetworks(@NonNull Context context) {
+            return ConnectivitySettingsManager.getUidsAllowedOnRestrictedNetworks(context);
+        }
+
+        /**
+         * Register ContentObserver for given Uri.
+         */
+        public void registerContentObserver(@NonNull Context context, @NonNull Uri uri,
+                boolean notifyForDescendants, @NonNull ContentObserver observer) {
+            context.getContentResolver().registerContentObserver(
+                    uri, notifyForDescendants, observer);
+        }
+    }
+
+    public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd) {
+        this(context, netd, new Dependencies());
+    }
+
+    @VisibleForTesting
+    PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
+            @NonNull final Dependencies deps) {
+        mPackageManager = context.getPackageManager();
+        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        mSystemConfigManager = context.getSystemService(SystemConfigManager.class);
+        mNetd = netd;
+        mDeps = deps;
+        mContext = context;
+    }
+
+    // Intended to be called only once at startup, after the system is ready. Installs a broadcast
+    // receiver to monitor ongoing UID changes, so this shouldn't/needn't be called again.
+    public synchronized void startMonitoring() {
+        log("Monitoring");
+
+        final Context userAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */);
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+        intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        intentFilter.addDataScheme("package");
+        userAllContext.registerReceiver(
+                mIntentReceiver, intentFilter, null /* broadcastPermission */,
+                null /* scheduler */);
+
+        // Register UIDS_ALLOWED_ON_RESTRICTED_NETWORKS setting observer
+        mDeps.registerContentObserver(
+                userAllContext,
+                Settings.Global.getUriFor(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS),
+                false /* notifyForDescendants */,
+                new ContentObserver(null) {
+                    @Override
+                    public void onChange(boolean selfChange) {
+                        onSettingChanged();
+                    }
+                });
+
+        // Read UIDS_ALLOWED_ON_RESTRICTED_NETWORKS setting and update
+        // mUidsAllowedOnRestrictedNetworks.
+        updateUidsAllowedOnRestrictedNetworks(mDeps.getUidsAllowedOnRestrictedNetworks(mContext));
+
+        List<PackageInfo> apps = mPackageManager.getInstalledPackages(GET_PERMISSIONS
+                | MATCH_ANY_USER);
+        if (apps == null) {
+            loge("No apps");
+            return;
+        }
+
+        SparseIntArray netdPermsUids = new SparseIntArray();
+
+        for (PackageInfo app : apps) {
+            int uid = app.applicationInfo != null ? app.applicationInfo.uid : INVALID_UID;
+            if (uid < 0) {
+                continue;
+            }
+            mAllApps.add(UserHandle.getAppId(uid));
+
+            boolean isNetwork = hasNetworkPermission(app);
+            boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
+
+            if (isNetwork || hasRestrictedPermission) {
+                Boolean permission = mApps.get(uid);
+                // If multiple packages share a UID (cf: android:sharedUserId) and ask for different
+                // permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is).
+                if (permission == null || permission == NETWORK) {
+                    mApps.put(uid, hasRestrictedPermission);
+                }
+            }
+
+            //TODO: unify the management of the permissions into one codepath.
+            int otherNetdPerms = getNetdPermissionMask(app.requestedPermissions,
+                    app.requestedPermissionsFlags);
+            netdPermsUids.put(uid, netdPermsUids.get(uid) | otherNetdPerms);
+        }
+
+        mUsers.addAll(mUserManager.getUserHandles(true /* excludeDying */));
+
+        final SparseArray<String> netdPermToSystemPerm = new SparseArray<>();
+        netdPermToSystemPerm.put(INetd.PERMISSION_INTERNET, INTERNET);
+        netdPermToSystemPerm.put(INetd.PERMISSION_UPDATE_DEVICE_STATS, UPDATE_DEVICE_STATS);
+        for (int i = 0; i < netdPermToSystemPerm.size(); i++) {
+            final int netdPermission = netdPermToSystemPerm.keyAt(i);
+            final String systemPermission = netdPermToSystemPerm.valueAt(i);
+            final int[] hasPermissionUids =
+                    mSystemConfigManager.getSystemPermissionUids(systemPermission);
+            for (int j = 0; j < hasPermissionUids.length; j++) {
+                final int uid = hasPermissionUids[j];
+                netdPermsUids.put(uid, netdPermsUids.get(uid) | netdPermission);
+            }
+        }
+        log("Users: " + mUsers.size() + ", Apps: " + mApps.size());
+        update(mUsers, mApps, true);
+        sendPackagePermissionsToNetd(netdPermsUids);
+    }
+
+    @VisibleForTesting
+    synchronized void updateUidsAllowedOnRestrictedNetworks(final Set<Integer> uids) {
+        mUidsAllowedOnRestrictedNetworks.clear();
+        mUidsAllowedOnRestrictedNetworks.addAll(uids);
+    }
+
+    @VisibleForTesting
+    static boolean isVendorApp(@NonNull ApplicationInfo appInfo) {
+        return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct();
+    }
+
+    @VisibleForTesting
+    boolean isCarryoverPackage(final ApplicationInfo appInfo) {
+        if (appInfo == null) return false;
+        return (appInfo.targetSdkVersion < VERSION_Q && isVendorApp(appInfo))
+                // Backward compatibility for b/114245686, on devices that launched before Q daemons
+                // and apps running as the system UID are exempted from this check.
+                || (appInfo.uid == SYSTEM_UID && mDeps.getDeviceFirstSdkInt() < VERSION_Q);
+    }
+
+    @VisibleForTesting
+    synchronized boolean isUidAllowedOnRestrictedNetworks(final ApplicationInfo appInfo) {
+        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);
+    }
+
+    @VisibleForTesting
+    boolean hasPermission(@NonNull final PackageInfo app, @NonNull final String permission) {
+        if (app.requestedPermissions == null || app.requestedPermissionsFlags == null) {
+            return false;
+        }
+        final int index = CollectionUtils.indexOf(app.requestedPermissions, permission);
+        if (index < 0 || index >= app.requestedPermissionsFlags.length) return false;
+        return (app.requestedPermissionsFlags[index] & REQUESTED_PERMISSION_GRANTED) != 0;
+    }
+
+    @VisibleForTesting
+    boolean hasNetworkPermission(@NonNull final PackageInfo app) {
+        return hasPermission(app, CHANGE_NETWORK_STATE);
+    }
+
+    @VisibleForTesting
+    boolean hasRestrictedNetworkPermission(@NonNull final PackageInfo app) {
+        // TODO : remove carryover package check in the future(b/31479477). All apps should just
+        //  request the appropriate permission for their use case since android Q.
+        return isCarryoverPackage(app.applicationInfo)
+                || isUidAllowedOnRestrictedNetworks(app.applicationInfo)
+                || hasPermission(app, PERMISSION_MAINLINE_NETWORK_STACK)
+                || hasPermission(app, NETWORK_STACK)
+                || hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+    }
+
+    /** Returns whether the given uid has using background network permission. */
+    public synchronized boolean hasUseBackgroundNetworksPermission(final int uid) {
+        // Apps with any of the CHANGE_NETWORK_STATE, NETWORK_STACK, CONNECTIVITY_INTERNAL or
+        // CONNECTIVITY_USE_RESTRICTED_NETWORKS permission has the permission to use background
+        // networks. mApps contains the result of checks for both hasNetworkPermission and
+        // hasRestrictedNetworkPermission. If uid is in the mApps list that means uid has one of
+        // permissions at least.
+        return mApps.containsKey(uid);
+    }
+
+    /**
+     * Returns whether the given uid has permission to use restricted networks.
+     */
+    public synchronized boolean hasRestrictedNetworksPermission(int uid) {
+        return Boolean.TRUE.equals(mApps.get(uid));
+    }
+
+    private void update(Set<UserHandle> users, Map<Integer, Boolean> apps, boolean add) {
+        List<Integer> network = new ArrayList<>();
+        List<Integer> system = new ArrayList<>();
+        for (Entry<Integer, Boolean> app : apps.entrySet()) {
+            List<Integer> list = app.getValue() ? system : network;
+            for (UserHandle user : users) {
+                if (user == null) continue;
+
+                list.add(user.getUid(app.getKey()));
+            }
+        }
+        try {
+            if (add) {
+                mNetd.networkSetPermissionForUser(INetd.PERMISSION_NETWORK, toIntArray(network));
+                mNetd.networkSetPermissionForUser(INetd.PERMISSION_SYSTEM, toIntArray(system));
+            } else {
+                mNetd.networkClearPermissionForUser(toIntArray(network));
+                mNetd.networkClearPermissionForUser(toIntArray(system));
+            }
+        } catch (RemoteException e) {
+            loge("Exception when updating permissions: " + e);
+        }
+    }
+
+    /**
+     * Called when a user is added. See {link #ACTION_USER_ADDED}.
+     *
+     * @param user The integer userHandle of the added user. See {@link #EXTRA_USER_HANDLE}.
+     *
+     * @hide
+     */
+    public synchronized void onUserAdded(@NonNull UserHandle user) {
+        mUsers.add(user);
+
+        Set<UserHandle> users = new HashSet<>();
+        users.add(user);
+        update(users, mApps, true);
+    }
+
+    /**
+     * Called when an user is removed. See {link #ACTION_USER_REMOVED}.
+     *
+     * @param user The integer userHandle of the removed user. See {@link #EXTRA_USER_HANDLE}.
+     *
+     * @hide
+     */
+    public synchronized void onUserRemoved(@NonNull UserHandle user) {
+        mUsers.remove(user);
+
+        Set<UserHandle> users = new HashSet<>();
+        users.add(user);
+        update(users, mApps, false);
+    }
+
+    /**
+     * Compare the current network permission and the given package's permission to find out highest
+     * permission for the uid.
+     *
+     * @param currentPermission Current uid network permission
+     * @param name The package has same uid that need compare its permission to update uid network
+     *             permission.
+     */
+    @VisibleForTesting
+    protected Boolean highestPermissionForUid(Boolean currentPermission, String name) {
+        if (currentPermission == SYSTEM) {
+            return currentPermission;
+        }
+        try {
+            final PackageInfo app = mPackageManager.getPackageInfo(name,
+                    GET_PERMISSIONS | MATCH_ANY_USER);
+            final boolean isNetwork = hasNetworkPermission(app);
+            final boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
+            if (isNetwork || hasRestrictedPermission) {
+                currentPermission = hasRestrictedPermission;
+            }
+        } catch (NameNotFoundException e) {
+            // App not found.
+            loge("NameNotFoundException " + name);
+        }
+        return currentPermission;
+    }
+
+    private int getPermissionForUid(final int uid) {
+        int permission = INetd.PERMISSION_NONE;
+        // Check all the packages for this UID. The UID has the permission if any of the
+        // packages in it has the permission.
+        final String[] packages = mPackageManager.getPackagesForUid(uid);
+        if (packages != null && packages.length > 0) {
+            for (String name : packages) {
+                final PackageInfo app = getPackageInfo(name);
+                if (app != null && app.requestedPermissions != null) {
+                    permission |= getNetdPermissionMask(app.requestedPermissions,
+                            app.requestedPermissionsFlags);
+                }
+            }
+        } else {
+            // The last package of this uid is removed from device. Clean the package up.
+            permission = INetd.PERMISSION_UNINSTALLED;
+        }
+        return permission;
+    }
+
+    /**
+     * Called when a package is added.
+     *
+     * @param packageName The name of the new package.
+     * @param uid The uid of the new package.
+     *
+     * @hide
+     */
+    public synchronized void onPackageAdded(@NonNull final String packageName, final int uid) {
+        // TODO: Netd is using appId for checking traffic permission. Correct the methods that are
+        //  using appId instead of uid actually
+        sendPackagePermissionsForUid(UserHandle.getAppId(uid), getPermissionForUid(uid));
+
+        // If multiple packages share a UID (cf: android:sharedUserId) and ask for different
+        // permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is).
+        final Boolean permission = highestPermissionForUid(mApps.get(uid), packageName);
+        if (permission != mApps.get(uid)) {
+            mApps.put(uid, permission);
+
+            Map<Integer, Boolean> apps = new HashMap<>();
+            apps.put(uid, permission);
+            update(mUsers, apps, true);
+        }
+
+        // If the newly-installed package falls within some VPN's uid range, update Netd with it.
+        // This needs to happen after the mApps update above, since removeBypassingUids() depends
+        // on mApps to check if the package can bypass VPN.
+        for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+            if (UidRange.containsUid(vpn.getValue(), uid)) {
+                final Set<Integer> changedUids = new HashSet<>();
+                changedUids.add(uid);
+                removeBypassingUids(changedUids, /* vpnAppUid */ -1);
+                updateVpnUids(vpn.getKey(), changedUids, true);
+            }
+        }
+        mAllApps.add(UserHandle.getAppId(uid));
+    }
+
+    private Boolean highestUidNetworkPermission(int uid) {
+        Boolean permission = null;
+        final String[] packages = mPackageManager.getPackagesForUid(uid);
+        if (!CollectionUtils.isEmpty(packages)) {
+            for (String name : packages) {
+                // If multiple packages have the same UID, give the UID all permissions that
+                // any package in that UID has.
+                permission = highestPermissionForUid(permission, name);
+                if (permission == SYSTEM) {
+                    break;
+                }
+            }
+        }
+        return permission;
+    }
+
+    /**
+     * Called when a package is removed.
+     *
+     * @param packageName The name of the removed package or null.
+     * @param uid containing the integer uid previously assigned to the package.
+     *
+     * @hide
+     */
+    public synchronized void onPackageRemoved(@NonNull final String packageName, final int uid) {
+        // TODO: Netd is using appId for checking traffic permission. Correct the methods that are
+        //  using appId instead of uid actually
+        sendPackagePermissionsForUid(UserHandle.getAppId(uid), getPermissionForUid(uid));
+
+        // If the newly-removed package falls within some VPN's uid range, update Netd with it.
+        // This needs to happen before the mApps update below, since removeBypassingUids() depends
+        // on mApps to check if the package can bypass VPN.
+        for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+            if (UidRange.containsUid(vpn.getValue(), uid)) {
+                final Set<Integer> changedUids = new HashSet<>();
+                changedUids.add(uid);
+                removeBypassingUids(changedUids, /* vpnAppUid */ -1);
+                updateVpnUids(vpn.getKey(), changedUids, false);
+            }
+        }
+        // If the package has been removed from all users on the device, clear it form mAllApps.
+        if (mPackageManager.getNameForUid(uid) == null) {
+            mAllApps.remove(UserHandle.getAppId(uid));
+        }
+
+        Map<Integer, Boolean> apps = new HashMap<>();
+        final Boolean permission = highestUidNetworkPermission(uid);
+        if (permission == SYSTEM) {
+            // An app with this UID still has the SYSTEM permission.
+            // Therefore, this UID must already have the SYSTEM permission.
+            // Nothing to do.
+            return;
+        }
+
+        if (permission == mApps.get(uid)) {
+            // The permissions of this UID have not changed. Nothing to do.
+            return;
+        } else if (permission != null) {
+            mApps.put(uid, permission);
+            apps.put(uid, permission);
+            update(mUsers, apps, true);
+        } else {
+            mApps.remove(uid);
+            apps.put(uid, NETWORK);  // doesn't matter which permission we pick here
+            update(mUsers, apps, false);
+        }
+    }
+
+    private static int getNetdPermissionMask(String[] requestedPermissions,
+                                             int[] requestedPermissionsFlags) {
+        int permissions = 0;
+        if (requestedPermissions == null || requestedPermissionsFlags == null) return permissions;
+        for (int i = 0; i < requestedPermissions.length; i++) {
+            if (requestedPermissions[i].equals(INTERNET)
+                    && ((requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) != 0)) {
+                permissions |= INetd.PERMISSION_INTERNET;
+            }
+            if (requestedPermissions[i].equals(UPDATE_DEVICE_STATS)
+                    && ((requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) != 0)) {
+                permissions |= INetd.PERMISSION_UPDATE_DEVICE_STATS;
+            }
+        }
+        return permissions;
+    }
+
+    private PackageInfo getPackageInfo(String packageName) {
+        try {
+            PackageInfo app = mPackageManager.getPackageInfo(packageName, GET_PERMISSIONS
+                    | MATCH_ANY_USER);
+            return app;
+        } catch (NameNotFoundException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 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 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,
+            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.
+        final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps);
+        removeBypassingUids(changedUids, vpnAppUid);
+        updateVpnUids(iface, changedUids, true);
+        if (mVpnUidRanges.containsKey(iface)) {
+            mVpnUidRanges.get(iface).addAll(rangesToAdd);
+        } else {
+            mVpnUidRanges.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 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,
+            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);
+        updateVpnUids(iface, changedUids, false);
+        Set<UidRange> existingRanges = mVpnUidRanges.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);
+        }
+    }
+
+    /**
+     * Compute the intersection of a set of UidRanges and appIds. Returns a set of uids
+     * that satisfies:
+     *   1. falls into one of the UidRange
+     *   2. matches one of the appIds
+     */
+    private Set<Integer> intersectUids(Set<UidRange> ranges, Set<Integer> appIds) {
+        Set<Integer> result = new HashSet<>();
+        for (UidRange range : ranges) {
+            for (int userId = range.getStartUser(); userId <= range.getEndUser(); userId++) {
+                for (int appId : appIds) {
+                    final UserHandle handle = UserHandle.of(userId);
+                    if (handle == null) continue;
+
+                    final int uid = handle.getUid(appId);
+                    if (range.contains(uid)) {
+                        result.add(uid);
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Remove all apps which can elect to bypass the VPN from the list of uids
+     *
+     * An app can elect to bypass the VPN if it hold SYSTEM permission, or if its the active VPN
+     * app itself.
+     *
+     * @param uids The list of uids to operate on
+     * @param vpnAppUid The uid of the VPN app
+     */
+    private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
+        uids.remove(vpnAppUid);
+        uids.removeIf(uid -> mApps.getOrDefault(uid, NETWORK) == SYSTEM);
+    }
+
+    /**
+     * Update netd about the list of uids that are under an active VPN connection which they cannot
+     * bypass.
+     *
+     * 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).
+     *
+     * @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
+     *        are to be removed from the interface.
+     */
+    private void updateVpnUids(String iface, Set<Integer> uids, boolean add) {
+        if (uids.size() == 0) {
+            return;
+        }
+        try {
+            if (add) {
+                mNetd.firewallAddUidInterfaceRules(iface, toIntArray(uids));
+            } else {
+                mNetd.firewallRemoveUidInterfaceRules(toIntArray(uids));
+            }
+        } catch (ServiceSpecificException e) {
+            // Silently ignore exception when device does not support eBPF, otherwise just log
+            // the exception and do not crash
+            if (e.errorCode != OsConstants.EOPNOTSUPP) {
+                loge("Exception when updating permissions: ", e);
+            }
+        } catch (RemoteException e) {
+            loge("Exception when updating permissions: ", e);
+        }
+    }
+
+    /**
+     * Called by PackageListObserver when a package is installed/uninstalled. Send the updated
+     * permission information to netd.
+     *
+     * @param uid the app uid of the package installed
+     * @param permissions the permissions the app requested and netd cares about.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    void sendPackagePermissionsForUid(int uid, int permissions) {
+        SparseIntArray netdPermissionsAppIds = new SparseIntArray();
+        netdPermissionsAppIds.put(uid, permissions);
+        sendPackagePermissionsToNetd(netdPermissionsAppIds);
+    }
+
+    /**
+     * Called by packageManagerService to send IPC to netd. Grant or revoke the INTERNET
+     * and/or UPDATE_DEVICE_STATS permission of the uids in array.
+     *
+     * @param netdPermissionsAppIds integer pairs of uids and the permission granted to it. If the
+     * permission is 0, revoke all permissions of that uid.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    void sendPackagePermissionsToNetd(SparseIntArray netdPermissionsAppIds) {
+        if (mNetd == null) {
+            Log.e(TAG, "Failed to get the netd service");
+            return;
+        }
+        ArrayList<Integer> allPermissionAppIds = new ArrayList<>();
+        ArrayList<Integer> internetPermissionAppIds = new ArrayList<>();
+        ArrayList<Integer> updateStatsPermissionAppIds = new ArrayList<>();
+        ArrayList<Integer> noPermissionAppIds = new ArrayList<>();
+        ArrayList<Integer> uninstalledAppIds = new ArrayList<>();
+        for (int i = 0; i < netdPermissionsAppIds.size(); i++) {
+            int permissions = netdPermissionsAppIds.valueAt(i);
+            switch(permissions) {
+                case (INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS):
+                    allPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
+                    break;
+                case INetd.PERMISSION_INTERNET:
+                    internetPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
+                    break;
+                case INetd.PERMISSION_UPDATE_DEVICE_STATS:
+                    updateStatsPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
+                    break;
+                case INetd.PERMISSION_NONE:
+                    noPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
+                    break;
+                case INetd.PERMISSION_UNINSTALLED:
+                    uninstalledAppIds.add(netdPermissionsAppIds.keyAt(i));
+                    break;
+                default:
+                    Log.e(TAG, "unknown permission type: " + permissions + "for uid: "
+                            + netdPermissionsAppIds.keyAt(i));
+            }
+        }
+        try {
+            // TODO: add a lock inside netd to protect IPC trafficSetNetPermForUids()
+            if (allPermissionAppIds.size() != 0) {
+                mNetd.trafficSetNetPermForUids(
+                        INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS,
+                        toIntArray(allPermissionAppIds));
+            }
+            if (internetPermissionAppIds.size() != 0) {
+                mNetd.trafficSetNetPermForUids(INetd.PERMISSION_INTERNET,
+                        toIntArray(internetPermissionAppIds));
+            }
+            if (updateStatsPermissionAppIds.size() != 0) {
+                mNetd.trafficSetNetPermForUids(INetd.PERMISSION_UPDATE_DEVICE_STATS,
+                        toIntArray(updateStatsPermissionAppIds));
+            }
+            if (noPermissionAppIds.size() != 0) {
+                mNetd.trafficSetNetPermForUids(INetd.PERMISSION_NONE,
+                        toIntArray(noPermissionAppIds));
+            }
+            if (uninstalledAppIds.size() != 0) {
+                mNetd.trafficSetNetPermForUids(INetd.PERMISSION_UNINSTALLED,
+                        toIntArray(uninstalledAppIds));
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Pass appId list of special permission failed." + e);
+        }
+    }
+
+    /** Should only be used by unit tests */
+    @VisibleForTesting
+    public Set<UidRange> getVpnUidRanges(String iface) {
+        return mVpnUidRanges.get(iface);
+    }
+
+    private synchronized void onSettingChanged() {
+        // Step1. Update uids allowed to use restricted networks and compute the set of uids to
+        // update.
+        final Set<Integer> uidsToUpdate = new ArraySet<>(mUidsAllowedOnRestrictedNetworks);
+        updateUidsAllowedOnRestrictedNetworks(mDeps.getUidsAllowedOnRestrictedNetworks(mContext));
+        uidsToUpdate.addAll(mUidsAllowedOnRestrictedNetworks);
+
+        final Map<Integer, Boolean> updatedUids = new HashMap<>();
+        final Map<Integer, Boolean> removedUids = new HashMap<>();
+
+        // Step2. For each uid to update, find out its new permission.
+        for (Integer uid : uidsToUpdate) {
+            final Boolean permission = highestUidNetworkPermission(uid);
+
+            if (null == permission) {
+                removedUids.put(uid, NETWORK); // Doesn't matter which permission is set here.
+                mApps.remove(uid);
+            } else {
+                updatedUids.put(uid, permission);
+                mApps.put(uid, permission);
+            }
+        }
+
+        // Step3. Update or revoke permission for uids with netd.
+        update(mUsers, updatedUids, true /* add */);
+        update(mUsers, removedUids, false /* add */);
+    }
+
+    /** Dump info to dumpsys */
+    public void dump(IndentingPrintWriter pw) {
+        pw.println("Interface filtering rules:");
+        pw.increaseIndent();
+        for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+            pw.println("Interface: " + vpn.getKey());
+            pw.println("UIDs: " + vpn.getValue().toString());
+            pw.println();
+        }
+        pw.decreaseIndent();
+    }
+
+    private static void log(String s) {
+        if (DBG) {
+            Log.d(TAG, s);
+        }
+    }
+
+    private static void loge(String s) {
+        Log.e(TAG, s);
+    }
+
+    private static void loge(String s, Throwable e) {
+        Log.e(TAG, s, e);
+    }
+}
diff --git a/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java b/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java
new file mode 100644
index 0000000..dd2815d
--- /dev/null
+++ b/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java
@@ -0,0 +1,87 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkCapabilities;
+import android.os.UserHandle;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A data class containing all the per-profile network preferences.
+ *
+ * A given profile can only have one preference.
+ */
+public class ProfileNetworkPreferences {
+    /**
+     * A single preference, as it applies to a given user profile.
+     */
+    public static class Preference {
+        @NonNull public final UserHandle user;
+        // Capabilities are only null when sending an object to remove the setting for a user
+        @Nullable public final NetworkCapabilities capabilities;
+
+        public Preference(@NonNull final UserHandle user,
+                @Nullable final NetworkCapabilities capabilities) {
+            this.user = user;
+            this.capabilities = null == capabilities ? null : new NetworkCapabilities(capabilities);
+        }
+
+        /** toString */
+        public String toString() {
+            return "[ProfileNetworkPreference user=" + user + " caps=" + capabilities + "]";
+        }
+    }
+
+    @NonNull public final List<Preference> preferences;
+
+    public ProfileNetworkPreferences() {
+        preferences = Collections.EMPTY_LIST;
+    }
+
+    private ProfileNetworkPreferences(@NonNull final List<Preference> list) {
+        preferences = Collections.unmodifiableList(list);
+    }
+
+    /**
+     * 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.
+     */
+    public ProfileNetworkPreferences 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);
+            }
+        }
+        if (null != pref.capabilities) {
+            newPrefs.add(pref);
+        }
+        return new ProfileNetworkPreferences(newPrefs);
+    }
+
+    public boolean isEmpty() {
+        return preferences.isEmpty();
+    }
+}
diff --git a/service/src/com/android/server/connectivity/ProxyTracker.java b/service/src/com/android/server/connectivity/ProxyTracker.java
new file mode 100644
index 0000000..f572b46
--- /dev/null
+++ b/service/src/com/android/server/connectivity/ProxyTracker.java
@@ -0,0 +1,353 @@
+/**
+ * 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.connectivity;
+
+import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_EXCLUSION_LIST;
+import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_HOST;
+import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_PAC;
+import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_PORT;
+import static android.provider.Settings.Global.HTTP_PROXY;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Network;
+import android.net.PacProxyManager;
+import android.net.Proxy;
+import android.net.ProxyInfo;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.net.module.util.ProxyUtils;
+
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * A class to handle proxy for ConnectivityService.
+ *
+ * @hide
+ */
+public class ProxyTracker {
+    private static final String TAG = ProxyTracker.class.getSimpleName();
+    private static final boolean DBG = true;
+
+    @NonNull
+    private final Context mContext;
+
+    @NonNull
+    private final Object mProxyLock = new Object();
+    // The global proxy is the proxy that is set device-wide, overriding any network-specific
+    // proxy. Note however that proxies are hints ; the system does not enforce their use. Hence
+    // this value is only for querying.
+    @Nullable
+    @GuardedBy("mProxyLock")
+    private ProxyInfo mGlobalProxy = null;
+    // The default proxy is the proxy that applies to no particular network if the global proxy
+    // is not set. Individual networks have their own settings that override this. This member
+    // is set through setDefaultProxy, which is called when the default network changes proxies
+    // in its LinkProperties, or when ConnectivityService switches to a new default network, or
+    // when PacProxyService resolves the proxy.
+    @Nullable
+    @GuardedBy("mProxyLock")
+    private volatile ProxyInfo mDefaultProxy = null;
+    // Whether the default proxy is enabled.
+    @GuardedBy("mProxyLock")
+    private boolean mDefaultProxyEnabled = true;
+
+    private final Handler mConnectivityServiceHandler;
+
+    private final PacProxyManager mPacProxyManager;
+
+    private class PacProxyInstalledListener implements PacProxyManager.PacProxyInstalledListener {
+        private final int mEvent;
+
+        PacProxyInstalledListener(int event) {
+            mEvent = event;
+        }
+
+        public void onPacProxyInstalled(@Nullable Network network, @NonNull ProxyInfo proxy) {
+            mConnectivityServiceHandler
+                    .sendMessage(mConnectivityServiceHandler
+                    .obtainMessage(mEvent, new Pair<>(network, proxy)));
+        }
+    }
+
+    public ProxyTracker(@NonNull final Context context,
+            @NonNull final Handler connectivityServiceInternalHandler, final int pacChangedEvent) {
+        mContext = context;
+        mConnectivityServiceHandler = connectivityServiceInternalHandler;
+        mPacProxyManager = context.getSystemService(PacProxyManager.class);
+
+        PacProxyInstalledListener listener = new PacProxyInstalledListener(pacChangedEvent);
+        mPacProxyManager.addPacProxyInstalledListener(
+                mConnectivityServiceHandler::post, listener);
+    }
+
+    // Convert empty ProxyInfo's to null as null-checks are used to determine if proxies are present
+    // (e.g. if mGlobalProxy==null fall back to network-specific proxy, if network-specific
+    // proxy is null then there is no proxy in place).
+    @Nullable
+    private static ProxyInfo canonicalizeProxyInfo(@Nullable final ProxyInfo proxy) {
+        if (proxy != null && TextUtils.isEmpty(proxy.getHost())
+                && Uri.EMPTY.equals(proxy.getPacFileUrl())) {
+            return null;
+        }
+        return proxy;
+    }
+
+    // ProxyInfo equality functions with a couple modifications over ProxyInfo.equals() to make it
+    // better for determining if a new proxy broadcast is necessary:
+    // 1. Canonicalize empty ProxyInfos to null so an empty proxy compares equal to null so as to
+    //    avoid unnecessary broadcasts.
+    // 2. Make sure all parts of the ProxyInfo's compare true, including the host when a PAC URL
+    //    is in place.  This is important so legacy PAC resolver (see com.android.proxyhandler)
+    //    changes aren't missed.  The legacy PAC resolver pretends to be a simple HTTP proxy but
+    //    actually uses the PAC to resolve; this results in ProxyInfo's with PAC URL, host and port
+    //    all set.
+    public static boolean proxyInfoEqual(@Nullable final ProxyInfo a, @Nullable final ProxyInfo b) {
+        final ProxyInfo pa = canonicalizeProxyInfo(a);
+        final ProxyInfo pb = canonicalizeProxyInfo(b);
+        // ProxyInfo.equals() doesn't check hosts when PAC URLs are present, but we need to check
+        // hosts even when PAC URLs are present to account for the legacy PAC resolver.
+        return Objects.equals(pa, pb) && (pa == null || Objects.equals(pa.getHost(), pb.getHost()));
+    }
+
+    /**
+     * Gets the default system-wide proxy.
+     *
+     * This will return the global proxy if set, otherwise the default proxy if in use. Note
+     * that this is not necessarily the proxy that any given process should use, as the right
+     * proxy for a process is the proxy for the network this process will use, which may be
+     * different from this value. This value is simply the default in case there is no proxy set
+     * in the network that will be used by a specific process.
+     * @return The default system-wide proxy or null if none.
+     */
+    @Nullable
+    public ProxyInfo getDefaultProxy() {
+        // This information is already available as a world read/writable jvm property.
+        synchronized (mProxyLock) {
+            if (mGlobalProxy != null) return mGlobalProxy;
+            if (mDefaultProxyEnabled) return mDefaultProxy;
+            return null;
+        }
+    }
+
+    /**
+     * Gets the global proxy.
+     *
+     * @return The global proxy or null if none.
+     */
+    @Nullable
+    public ProxyInfo getGlobalProxy() {
+        // This information is already available as a world read/writable jvm property.
+        synchronized (mProxyLock) {
+            return mGlobalProxy;
+        }
+    }
+
+    /**
+     * Read the global proxy settings and cache them in memory.
+     */
+    public void loadGlobalProxy() {
+        if (loadDeprecatedGlobalHttpProxy()) {
+            return;
+        }
+        ContentResolver res = mContext.getContentResolver();
+        String host = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_HOST);
+        int port = Settings.Global.getInt(res, GLOBAL_HTTP_PROXY_PORT, 0);
+        String exclList = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST);
+        String pacFileUrl = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_PAC);
+        if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(pacFileUrl)) {
+            ProxyInfo proxyProperties;
+            if (!TextUtils.isEmpty(pacFileUrl)) {
+                proxyProperties = ProxyInfo.buildPacProxy(Uri.parse(pacFileUrl));
+            } else {
+                proxyProperties = ProxyInfo.buildDirectProxy(host, port,
+                        ProxyUtils.exclusionStringAsList(exclList));
+            }
+            if (!proxyProperties.isValid()) {
+                if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyProperties);
+                return;
+            }
+
+            synchronized (mProxyLock) {
+                mGlobalProxy = proxyProperties;
+            }
+
+            if (!TextUtils.isEmpty(pacFileUrl)) {
+                mConnectivityServiceHandler.post(
+                        () -> mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));
+            }
+        }
+    }
+
+    /**
+     * Read the global proxy from the deprecated Settings.Global.HTTP_PROXY setting and apply it.
+     * Returns {@code true} when global proxy was set successfully from deprecated setting.
+     */
+    public boolean loadDeprecatedGlobalHttpProxy() {
+        final String proxy = Settings.Global.getString(mContext.getContentResolver(), HTTP_PROXY);
+        if (!TextUtils.isEmpty(proxy)) {
+            String data[] = proxy.split(":");
+            if (data.length == 0) {
+                return false;
+            }
+
+            final String proxyHost = data[0];
+            int proxyPort = 8080;
+            if (data.length > 1) {
+                try {
+                    proxyPort = Integer.parseInt(data[1]);
+                } catch (NumberFormatException e) {
+                    return false;
+                }
+            }
+            final ProxyInfo p = ProxyInfo.buildDirectProxy(proxyHost, proxyPort,
+                    Collections.emptyList());
+            setGlobalProxy(p);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Sends the system broadcast informing apps about a new proxy configuration.
+     *
+     * Confusingly this method also sets the PAC file URL. TODO : separate this, it has nothing
+     * to do in a "sendProxyBroadcast" method.
+     */
+    public void sendProxyBroadcast() {
+        final ProxyInfo defaultProxy = getDefaultProxy();
+        final ProxyInfo proxyInfo = null != defaultProxy ?
+                defaultProxy : ProxyInfo.buildDirectProxy("", 0, Collections.emptyList());
+        mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);
+
+        if (!shouldSendBroadcast(proxyInfo)) {
+            return;
+        }
+        if (DBG) Log.d(TAG, "sending Proxy Broadcast for " + proxyInfo);
+        Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
+        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING |
+                Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        intent.putExtra(Proxy.EXTRA_PROXY_INFO, proxyInfo);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private boolean shouldSendBroadcast(ProxyInfo proxy) {
+        return Uri.EMPTY.equals(proxy.getPacFileUrl()) || proxy.getPort() > 0;
+    }
+
+    /**
+     * Sets the global proxy in memory. Also writes the values to the global settings of the device.
+     *
+     * @param proxyInfo the proxy spec, or null for no proxy.
+     */
+    public void setGlobalProxy(@Nullable ProxyInfo proxyInfo) {
+        synchronized (mProxyLock) {
+            // ProxyInfo#equals is not commutative :( and is public API, so it can't be fixed.
+            if (proxyInfo == mGlobalProxy) return;
+            if (proxyInfo != null && proxyInfo.equals(mGlobalProxy)) return;
+            if (mGlobalProxy != null && mGlobalProxy.equals(proxyInfo)) return;
+
+            final String host;
+            final int port;
+            final String exclList;
+            final String pacFileUrl;
+            if (proxyInfo != null && (!TextUtils.isEmpty(proxyInfo.getHost()) ||
+                    !Uri.EMPTY.equals(proxyInfo.getPacFileUrl()))) {
+                if (!proxyInfo.isValid()) {
+                    if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
+                    return;
+                }
+                mGlobalProxy = new ProxyInfo(proxyInfo);
+                host = mGlobalProxy.getHost();
+                port = mGlobalProxy.getPort();
+                exclList = ProxyUtils.exclusionListAsString(mGlobalProxy.getExclusionList());
+                pacFileUrl = Uri.EMPTY.equals(proxyInfo.getPacFileUrl())
+                        ? "" : proxyInfo.getPacFileUrl().toString();
+            } else {
+                host = "";
+                port = 0;
+                exclList = "";
+                pacFileUrl = "";
+                mGlobalProxy = null;
+            }
+            final ContentResolver res = mContext.getContentResolver();
+            final long token = Binder.clearCallingIdentity();
+            try {
+                Settings.Global.putString(res, GLOBAL_HTTP_PROXY_HOST, host);
+                Settings.Global.putInt(res, GLOBAL_HTTP_PROXY_PORT, port);
+                Settings.Global.putString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST, exclList);
+                Settings.Global.putString(res, GLOBAL_HTTP_PROXY_PAC, pacFileUrl);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+
+            sendProxyBroadcast();
+        }
+    }
+
+    /**
+     * Sets the default proxy for the device.
+     *
+     * The default proxy is the proxy used for networks that do not have a specific proxy.
+     * @param proxyInfo the proxy spec, or null for no proxy.
+     */
+    public void setDefaultProxy(@Nullable ProxyInfo proxyInfo) {
+        synchronized (mProxyLock) {
+            if (Objects.equals(mDefaultProxy, proxyInfo)) return;
+            if (proxyInfo != null &&  !proxyInfo.isValid()) {
+                if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
+                return;
+            }
+
+            // This call could be coming from the PacProxyService, containing the port of the
+            // local proxy. If this new proxy matches the global proxy then copy this proxy to the
+            // global (to get the correct local port), and send a broadcast.
+            // TODO: Switch PacProxyService to have its own message to send back rather than
+            // reusing EVENT_HAS_CHANGED_PROXY and this call to handleApplyDefaultProxy.
+            if ((mGlobalProxy != null) && (proxyInfo != null)
+                    && (!Uri.EMPTY.equals(proxyInfo.getPacFileUrl()))
+                    && proxyInfo.getPacFileUrl().equals(mGlobalProxy.getPacFileUrl())) {
+                mGlobalProxy = proxyInfo;
+                sendProxyBroadcast();
+                return;
+            }
+            mDefaultProxy = proxyInfo;
+
+            if (mGlobalProxy != null) return;
+            if (mDefaultProxyEnabled) {
+                sendProxyBroadcast();
+            }
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
new file mode 100644
index 0000000..534dbe7
--- /dev/null
+++ b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
@@ -0,0 +1,199 @@
+/*
+ * 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.connectivity;
+
+import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE;
+
+import android.annotation.NonNull;
+import android.net.IQosCallback;
+import android.net.Network;
+import android.net.QosCallbackException;
+import android.net.QosFilter;
+import android.net.QosSession;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.telephony.data.EpsBearerQosSessionAttributes;
+import android.telephony.data.NrQosSessionAttributes;
+import android.util.Log;
+
+import java.util.Objects;
+
+/**
+ * Wraps callback related information and sends messages between network agent and the application.
+ * <p/>
+ * This is a satellite class of {@link com.android.server.ConnectivityService} and not meant
+ * to be used in other contexts.
+ *
+ * @hide
+ */
+class QosCallbackAgentConnection implements IBinder.DeathRecipient {
+    private static final String TAG = QosCallbackAgentConnection.class.getSimpleName();
+    private static final boolean DBG = false;
+
+    private final int mAgentCallbackId;
+    @NonNull private final QosCallbackTracker mQosCallbackTracker;
+    @NonNull private final IQosCallback mCallback;
+    @NonNull private final IBinder mBinder;
+    @NonNull private final QosFilter mFilter;
+    @NonNull private final NetworkAgentInfo mNetworkAgentInfo;
+
+    private final int mUid;
+
+    /**
+     * Gets the uid
+     * @return uid
+     */
+    int getUid() {
+        return mUid;
+    }
+
+    /**
+     * Gets the binder
+     * @return binder
+     */
+    @NonNull
+    IBinder getBinder() {
+        return mBinder;
+    }
+
+    /**
+     * Gets the callback id
+     *
+     * @return callback id
+     */
+    int getAgentCallbackId() {
+        return mAgentCallbackId;
+    }
+
+    /**
+     * Gets the network tied to the callback of this connection
+     *
+     * @return network
+     */
+    @NonNull
+    Network getNetwork() {
+        return mFilter.getNetwork();
+    }
+
+    QosCallbackAgentConnection(@NonNull final QosCallbackTracker qosCallbackTracker,
+            final int agentCallbackId,
+            @NonNull final IQosCallback callback,
+            @NonNull final QosFilter filter,
+            final int uid,
+            @NonNull final NetworkAgentInfo networkAgentInfo) {
+        Objects.requireNonNull(qosCallbackTracker, "qosCallbackTracker must be non-null");
+        Objects.requireNonNull(callback, "callback must be non-null");
+        Objects.requireNonNull(filter, "filter must be non-null");
+        Objects.requireNonNull(networkAgentInfo, "networkAgentInfo must be non-null");
+
+        mQosCallbackTracker = qosCallbackTracker;
+        mAgentCallbackId = agentCallbackId;
+        mCallback = callback;
+        mFilter = filter;
+        mUid = uid;
+        mBinder = mCallback.asBinder();
+        mNetworkAgentInfo = networkAgentInfo;
+    }
+
+    @Override
+    public void binderDied() {
+        logw("binderDied: binder died with callback id: " + mAgentCallbackId);
+        mQosCallbackTracker.unregisterCallback(mCallback);
+    }
+
+    void unlinkToDeathRecipient() {
+        mBinder.unlinkToDeath(this, 0);
+    }
+
+    // Returns false if the NetworkAgent was never notified.
+    boolean sendCmdRegisterCallback() {
+        final int exceptionType = mFilter.validate();
+        if (exceptionType != EX_TYPE_FILTER_NONE) {
+            try {
+                if (DBG) log("sendCmdRegisterCallback: filter validation failed");
+                mCallback.onError(exceptionType);
+            } catch (final RemoteException e) {
+                loge("sendCmdRegisterCallback:", e);
+            }
+            return false;
+        }
+
+        try {
+            mBinder.linkToDeath(this, 0);
+        } catch (final RemoteException e) {
+            loge("failed linking to death recipient", e);
+            return false;
+        }
+        mNetworkAgentInfo.onQosFilterCallbackRegistered(mAgentCallbackId, mFilter);
+        return true;
+    }
+
+    void sendCmdUnregisterCallback() {
+        if (DBG) log("sendCmdUnregisterCallback: unregistering");
+        mNetworkAgentInfo.onQosCallbackUnregistered(mAgentCallbackId);
+    }
+
+    void sendEventEpsQosSessionAvailable(final QosSession session,
+            final EpsBearerQosSessionAttributes attributes) {
+        try {
+            if (DBG) log("sendEventEpsQosSessionAvailable: sending...");
+            mCallback.onQosEpsBearerSessionAvailable(session, attributes);
+        } catch (final RemoteException e) {
+            loge("sendEventEpsQosSessionAvailable: remote exception", e);
+        }
+    }
+
+    void sendEventNrQosSessionAvailable(final QosSession session,
+            final NrQosSessionAttributes attributes) {
+        try {
+            if (DBG) log("sendEventNrQosSessionAvailable: sending...");
+            mCallback.onNrQosSessionAvailable(session, attributes);
+        } catch (final RemoteException e) {
+            loge("sendEventNrQosSessionAvailable: remote exception", e);
+        }
+    }
+
+    void sendEventQosSessionLost(@NonNull final QosSession session) {
+        try {
+            if (DBG) log("sendEventQosSessionLost: sending...");
+            mCallback.onQosSessionLost(session);
+        } catch (final RemoteException e) {
+            loge("sendEventQosSessionLost: remote exception", e);
+        }
+    }
+
+    void sendEventQosCallbackError(@QosCallbackException.ExceptionType final int exceptionType) {
+        try {
+            if (DBG) log("sendEventQosCallbackError: sending...");
+            mCallback.onError(exceptionType);
+        } catch (final RemoteException e) {
+            loge("sendEventQosCallbackError: remote exception", e);
+        }
+    }
+
+    private static void log(@NonNull final String msg) {
+        Log.d(TAG, msg);
+    }
+
+    private static void logw(@NonNull final String msg) {
+        Log.w(TAG, msg);
+    }
+
+    private static void loge(@NonNull final String msg, final Throwable t) {
+        Log.e(TAG, msg, t);
+    }
+}
diff --git a/service/src/com/android/server/connectivity/QosCallbackTracker.java b/service/src/com/android/server/connectivity/QosCallbackTracker.java
new file mode 100644
index 0000000..b6ab47b
--- /dev/null
+++ b/service/src/com/android/server/connectivity/QosCallbackTracker.java
@@ -0,0 +1,292 @@
+/*
+ * 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.connectivity;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.IQosCallback;
+import android.net.Network;
+import android.net.QosCallbackException;
+import android.net.QosFilter;
+import android.net.QosSession;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.telephony.data.EpsBearerQosSessionAttributes;
+import android.telephony.data.NrQosSessionAttributes;
+import android.util.Log;
+
+import com.android.net.module.util.CollectionUtils;
+import com.android.server.ConnectivityService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tracks qos callbacks and handles the communication between the network agent and application.
+ * <p/>
+ * Any method prefixed by handle must be called from the
+ * {@link com.android.server.ConnectivityService} handler thread.
+ *
+ * @hide
+ */
+public class QosCallbackTracker {
+    private static final String TAG = QosCallbackTracker.class.getSimpleName();
+    private static final boolean DBG = true;
+
+    @NonNull
+    private final Handler mConnectivityServiceHandler;
+
+    @NonNull
+    private final ConnectivityService.PerUidCounter mNetworkRequestCounter;
+
+    /**
+     * Each agent gets a unique callback id that is used to proxy messages back to the original
+     * callback.
+     * <p/>
+     * Note: The fact that this is initialized to 0 is to ensure that the thread running
+     * {@link #handleRegisterCallback(IQosCallback, QosFilter, int, NetworkAgentInfo)} sees the
+     * initialized value. This would not necessarily be the case if the value was initialized to
+     * the non-default value.
+     * <p/>
+     * Note: The term previous does not apply to the first callback id that is assigned.
+     */
+    private int mPreviousAgentCallbackId = 0;
+
+    @NonNull
+    private final List<QosCallbackAgentConnection> mConnections = new ArrayList<>();
+
+    /**
+     *
+     * @param connectivityServiceHandler must be the same handler used with
+     *                {@link com.android.server.ConnectivityService}
+     * @param networkRequestCounter keeps track of the number of open requests under a given
+     *                              uid
+     */
+    public QosCallbackTracker(@NonNull final Handler connectivityServiceHandler,
+            final ConnectivityService.PerUidCounter networkRequestCounter) {
+        mConnectivityServiceHandler = connectivityServiceHandler;
+        mNetworkRequestCounter = networkRequestCounter;
+    }
+
+    /**
+     * Registers the callback with the tracker
+     *
+     * @param callback the callback to register
+     * @param filter the filter being registered alongside the callback
+     */
+    public void registerCallback(@NonNull final IQosCallback callback,
+            @NonNull final QosFilter filter, @NonNull final NetworkAgentInfo networkAgentInfo) {
+        final int uid = Binder.getCallingUid();
+
+        // Enforce that the number of requests under this uid has exceeded the allowed number
+        mNetworkRequestCounter.incrementCountOrThrow(uid);
+
+        mConnectivityServiceHandler.post(
+                () -> handleRegisterCallback(callback, filter, uid, networkAgentInfo));
+    }
+
+    private void handleRegisterCallback(@NonNull final IQosCallback callback,
+            @NonNull final QosFilter filter, final int uid,
+            @NonNull final NetworkAgentInfo networkAgentInfo) {
+        final QosCallbackAgentConnection ac =
+                handleRegisterCallbackInternal(callback, filter, uid, networkAgentInfo);
+        if (ac != null) {
+            if (DBG) log("handleRegisterCallback: added callback " + ac.getAgentCallbackId());
+            mConnections.add(ac);
+        } else {
+            mNetworkRequestCounter.decrementCount(uid);
+        }
+    }
+
+    private QosCallbackAgentConnection handleRegisterCallbackInternal(
+            @NonNull final IQosCallback callback,
+            @NonNull final QosFilter filter, final int uid,
+            @NonNull final NetworkAgentInfo networkAgentInfo) {
+        final IBinder binder = callback.asBinder();
+        if (CollectionUtils.any(mConnections, c -> c.getBinder().equals(binder))) {
+            // A duplicate registration would have only made this far due to a programming error.
+            logwtf("handleRegisterCallback: Callbacks can only be register once.");
+            return null;
+        }
+
+        mPreviousAgentCallbackId = mPreviousAgentCallbackId + 1;
+        final int newCallbackId = mPreviousAgentCallbackId;
+
+        final QosCallbackAgentConnection ac =
+                new QosCallbackAgentConnection(this, newCallbackId, callback,
+                        filter, uid, networkAgentInfo);
+
+        final int exceptionType = filter.validate();
+        if (exceptionType != QosCallbackException.EX_TYPE_FILTER_NONE) {
+            ac.sendEventQosCallbackError(exceptionType);
+            return null;
+        }
+
+        // Only add to the callback maps if the NetworkAgent successfully registered it
+        if (!ac.sendCmdRegisterCallback()) {
+            // There was an issue when registering the agent
+            if (DBG) log("handleRegisterCallback: error sending register callback");
+            mNetworkRequestCounter.decrementCount(uid);
+            return null;
+        }
+        return ac;
+    }
+
+    /**
+     * Unregisters callback
+     * @param callback callback to unregister
+     */
+    public void unregisterCallback(@NonNull final IQosCallback callback) {
+        mConnectivityServiceHandler.post(() -> handleUnregisterCallback(callback.asBinder(), true));
+    }
+
+    private void handleUnregisterCallback(@NonNull final IBinder binder,
+            final boolean sendToNetworkAgent) {
+        final int connIndex =
+                CollectionUtils.indexOf(mConnections, c -> c.getBinder().equals(binder));
+        if (connIndex < 0) {
+            logw("handleUnregisterCallback: no matching agentConnection");
+            return;
+        }
+        final QosCallbackAgentConnection agentConnection = mConnections.get(connIndex);
+
+        if (DBG) {
+            log("handleUnregisterCallback: unregister "
+                    + agentConnection.getAgentCallbackId());
+        }
+
+        mNetworkRequestCounter.decrementCount(agentConnection.getUid());
+        mConnections.remove(agentConnection);
+
+        if (sendToNetworkAgent) {
+            agentConnection.sendCmdUnregisterCallback();
+        }
+        agentConnection.unlinkToDeathRecipient();
+    }
+
+    /**
+     * Called when the NetworkAgent sends the qos session available event for EPS
+     *
+     * @param qosCallbackId the callback id that the qos session is now available to
+     * @param session the qos session that is now available
+     * @param attributes the qos attributes that are now available on the qos session
+     */
+    public void sendEventEpsQosSessionAvailable(final int qosCallbackId,
+            final QosSession session,
+            final EpsBearerQosSessionAttributes attributes) {
+        runOnAgentConnection(qosCallbackId, "sendEventEpsQosSessionAvailable: ",
+                ac -> ac.sendEventEpsQosSessionAvailable(session, attributes));
+    }
+
+    /**
+     * Called when the NetworkAgent sends the qos session available event for NR
+     *
+     * @param qosCallbackId the callback id that the qos session is now available to
+     * @param session the qos session that is now available
+     * @param attributes the qos attributes that are now available on the qos session
+     */
+    public void sendEventNrQosSessionAvailable(final int qosCallbackId,
+            final QosSession session,
+            final NrQosSessionAttributes attributes) {
+        runOnAgentConnection(qosCallbackId, "sendEventNrQosSessionAvailable: ",
+                ac -> ac.sendEventNrQosSessionAvailable(session, attributes));
+    }
+
+    /**
+     * Called when the NetworkAgent sends the qos session lost event
+     *
+     * @param qosCallbackId the callback id that lost the qos session
+     * @param session the corresponding qos session
+     */
+    public void sendEventQosSessionLost(final int qosCallbackId,
+            final QosSession session) {
+        runOnAgentConnection(qosCallbackId, "sendEventQosSessionLost: ",
+                ac -> ac.sendEventQosSessionLost(session));
+    }
+
+    /**
+     * Called when the NetworkAgent sends the qos session on error event
+     *
+     * @param qosCallbackId the callback id that should receive the exception
+     * @param exceptionType the type of exception that caused the callback to error
+     */
+    public void sendEventQosCallbackError(final int qosCallbackId,
+            @QosCallbackException.ExceptionType final int exceptionType) {
+        runOnAgentConnection(qosCallbackId, "sendEventQosCallbackError: ",
+                ac -> {
+                    ac.sendEventQosCallbackError(exceptionType);
+                    handleUnregisterCallback(ac.getBinder(), false);
+                });
+    }
+
+    /**
+     * Unregisters all callbacks associated to this network agent
+     *
+     * Note: Must be called on the connectivity service handler thread
+     *
+     * @param network the network that was released
+     */
+    public void handleNetworkReleased(@Nullable final Network network) {
+        // Iterate in reverse order as agent connections will be removed when unregistering
+        for (int i = mConnections.size() - 1; i >= 0; i--) {
+            final QosCallbackAgentConnection agentConnection = mConnections.get(i);
+            if (!agentConnection.getNetwork().equals(network)) continue;
+            agentConnection.sendEventQosCallbackError(
+                    QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED);
+
+            // Call unregister workflow w\o sending anything to agent since it is disconnected.
+            handleUnregisterCallback(agentConnection.getBinder(), false);
+        }
+    }
+
+    private interface AgentConnectionAction {
+        void execute(@NonNull QosCallbackAgentConnection agentConnection);
+    }
+
+    @Nullable
+    private void runOnAgentConnection(final int qosCallbackId,
+            @NonNull final String logPrefix,
+            @NonNull final AgentConnectionAction action) {
+        mConnectivityServiceHandler.post(() -> {
+            final int acIndex = CollectionUtils.indexOf(mConnections,
+                            c -> c.getAgentCallbackId() == qosCallbackId);
+            if (acIndex == -1) {
+                loge(logPrefix + ": " + qosCallbackId + " missing callback id");
+                return;
+            }
+
+            action.execute(mConnections.get(acIndex));
+        });
+    }
+
+    private static void log(final String msg) {
+        Log.d(TAG, msg);
+    }
+
+    private static void logw(final String msg) {
+        Log.w(TAG, msg);
+    }
+
+    private static void loge(final String msg) {
+        Log.e(TAG, msg);
+    }
+
+    private static void logwtf(final String msg) {
+        Log.wtf(TAG, msg);
+    }
+}
diff --git a/service/src/com/android/server/connectivity/TcpKeepaliveController.java b/service/src/com/android/server/connectivity/TcpKeepaliveController.java
new file mode 100644
index 0000000..73f3475
--- /dev/null
+++ b/service/src/com/android/server/connectivity/TcpKeepaliveController.java
@@ -0,0 +1,340 @@
+/*
+ * 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 com.android.server.connectivity;
+
+import static android.net.SocketKeepalive.DATA_RECEIVED;
+import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
+import static android.net.SocketKeepalive.ERROR_SOCKET_NOT_IDLE;
+import static android.net.SocketKeepalive.ERROR_UNSUPPORTED;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+import static android.system.OsConstants.ENOPROTOOPT;
+import static android.system.OsConstants.FIONREAD;
+import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IP_TOS;
+import static android.system.OsConstants.IP_TTL;
+
+import static com.android.server.connectivity.OsCompat.TIOCOUTQ;
+
+import android.annotation.NonNull;
+import android.net.InvalidPacketException;
+import android.net.NetworkUtils;
+import android.net.SocketKeepalive.InvalidSocketException;
+import android.net.TcpKeepalivePacketData;
+import android.net.TcpKeepalivePacketDataParcelable;
+import android.net.TcpRepairWindow;
+import android.net.util.KeepalivePacketDataUtil;
+import android.os.Handler;
+import android.os.MessageQueue;
+import android.os.Messenger;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
+
+import java.io.FileDescriptor;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+
+/**
+ * Manage tcp socket which offloads tcp keepalive.
+ *
+ * The input socket will be changed to repair mode and the application
+ * will not have permission to read/write data. If the application wants
+ * to write data, it must stop tcp keepalive offload to leave repair mode
+ * first. If a remote packet arrives, repair mode will be turned off and
+ * offload will be stopped. The application will receive a callback to know
+ * it can start reading data.
+ *
+ * {start,stop}SocketMonitor are thread-safe, but care must be taken in the
+ * order in which they are called. Please note that while calling
+ * {@link #startSocketMonitor(FileDescriptor, Messenger, int)} multiple times
+ * with either the same slot or the same FileDescriptor without stopping it in
+ * between will result in an exception, calling {@link #stopSocketMonitor(int)}
+ * multiple times with the same int is explicitly a no-op.
+ * Please also note that switching the socket to repair mode is not synchronized
+ * with either of these operations and has to be done in an orderly fashion
+ * with stopSocketMonitor. Take care in calling these in the right order.
+ * @hide
+ */
+public class TcpKeepaliveController {
+    private static final String TAG = "TcpKeepaliveController";
+    private static final boolean DBG = false;
+
+    private final MessageQueue mFdHandlerQueue;
+
+    private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+
+    // Reference include/uapi/linux/tcp.h
+    private static final int TCP_REPAIR = 19;
+    private static final int TCP_REPAIR_QUEUE = 20;
+    private static final int TCP_QUEUE_SEQ = 21;
+    private static final int TCP_NO_QUEUE = 0;
+    private static final int TCP_RECV_QUEUE = 1;
+    private static final int TCP_SEND_QUEUE = 2;
+    private static final int TCP_REPAIR_OFF = 0;
+    private static final int TCP_REPAIR_ON = 1;
+    // Reference include/uapi/linux/sockios.h
+    private static final int SIOCINQ = FIONREAD;
+    private static final int SIOCOUTQ = TIOCOUTQ;
+
+    /**
+     * Keeps track of packet listeners.
+     * Key: slot number of keepalive offload.
+     * Value: {@link FileDescriptor} being listened to.
+     */
+    @GuardedBy("mListeners")
+    private final SparseArray<FileDescriptor> mListeners = new SparseArray<>();
+
+    public TcpKeepaliveController(final Handler connectivityServiceHandler) {
+        mFdHandlerQueue = connectivityServiceHandler.getLooper().getQueue();
+    }
+
+    /** Build tcp keepalive packet. */
+    public static TcpKeepalivePacketData getTcpKeepalivePacket(@NonNull FileDescriptor fd)
+            throws InvalidPacketException, InvalidSocketException {
+        try {
+            final TcpKeepalivePacketDataParcelable tcpDetails = switchToRepairMode(fd);
+            return KeepalivePacketDataUtil.fromStableParcelable(tcpDetails);
+        } catch (InvalidPacketException | InvalidSocketException e) {
+            switchOutOfRepairMode(fd);
+            throw e;
+        }
+    }
+    /**
+     * Switch the tcp socket to repair mode and query detail tcp information.
+     *
+     * @param fd the fd of socket on which to use keepalive offload.
+     * @return a {@link TcpKeepalivePacketDataParcelable} object for current
+     * tcp/ip information.
+     */
+    private static TcpKeepalivePacketDataParcelable switchToRepairMode(FileDescriptor fd)
+            throws InvalidSocketException {
+        if (DBG) Log.i(TAG, "switchToRepairMode to start tcp keepalive : " + fd);
+        final TcpKeepalivePacketDataParcelable tcpDetails = new TcpKeepalivePacketDataParcelable();
+        final SocketAddress srcSockAddr;
+        final SocketAddress dstSockAddr;
+        final TcpRepairWindow trw;
+
+        // Query source address and port.
+        try {
+            srcSockAddr = Os.getsockname(fd);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Get sockname fail: ", e);
+            throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+        }
+        if (srcSockAddr instanceof InetSocketAddress) {
+            tcpDetails.srcAddress = getAddress((InetSocketAddress) srcSockAddr);
+            tcpDetails.srcPort = getPort((InetSocketAddress) srcSockAddr);
+        } else {
+            Log.e(TAG, "Invalid or mismatched SocketAddress");
+            throw new InvalidSocketException(ERROR_INVALID_SOCKET);
+        }
+        // Query destination address and port.
+        try {
+            dstSockAddr = Os.getpeername(fd);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Get peername fail: ", e);
+            throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+        }
+        if (dstSockAddr instanceof InetSocketAddress) {
+            tcpDetails.dstAddress = getAddress((InetSocketAddress) dstSockAddr);
+            tcpDetails.dstPort = getPort((InetSocketAddress) dstSockAddr);
+        } else {
+            Log.e(TAG, "Invalid or mismatched peer SocketAddress");
+            throw new InvalidSocketException(ERROR_INVALID_SOCKET);
+        }
+
+        // Query sequence and ack number
+        dropAllIncomingPackets(fd, true);
+        try {
+            // Switch to tcp repair mode.
+            Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_ON);
+
+            // Check if socket is idle.
+            if (!isSocketIdle(fd)) {
+                Log.e(TAG, "Socket is not idle");
+                throw new InvalidSocketException(ERROR_SOCKET_NOT_IDLE);
+            }
+            // Query write sequence number from SEND_QUEUE.
+            Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_SEND_QUEUE);
+            tcpDetails.seq = OsCompat.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);
+            // Query read sequence number from RECV_QUEUE.
+            Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_RECV_QUEUE);
+            tcpDetails.ack = OsCompat.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);
+            // Switch to NO_QUEUE to prevent illegal socket read/write in repair mode.
+            Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_NO_QUEUE);
+            // Finally, check if socket is still idle. TODO : this check needs to move to
+            // after starting polling to prevent a race.
+            if (!isReceiveQueueEmpty(fd)) {
+                Log.e(TAG, "Fatal: receive queue of this socket is not empty");
+                throw new InvalidSocketException(ERROR_INVALID_SOCKET);
+            }
+            if (!isSendQueueEmpty(fd)) {
+                Log.e(TAG, "Socket is not idle");
+                throw new InvalidSocketException(ERROR_SOCKET_NOT_IDLE);
+            }
+
+            // Query tcp window size.
+            trw = NetworkUtils.getTcpRepairWindow(fd);
+            tcpDetails.rcvWnd = trw.rcvWnd;
+            tcpDetails.rcvWndScale = trw.rcvWndScale;
+            if (tcpDetails.srcAddress.length == 4 /* V4 address length */) {
+                // Query TOS.
+                tcpDetails.tos = OsCompat.getsockoptInt(fd, IPPROTO_IP, IP_TOS);
+                // Query TTL.
+                tcpDetails.ttl = OsCompat.getsockoptInt(fd, IPPROTO_IP, IP_TTL);
+            }
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Exception reading TCP state from socket", e);
+            if (e.errno == ENOPROTOOPT) {
+                // ENOPROTOOPT may happen in kernel version lower than 4.8.
+                // Treat it as ERROR_UNSUPPORTED.
+                throw new InvalidSocketException(ERROR_UNSUPPORTED, e);
+            } else {
+                throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+            }
+        } finally {
+            dropAllIncomingPackets(fd, false);
+        }
+
+        // Keepalive sequence number is last sequence number - 1. If it couldn't be retrieved,
+        // then it must be set to -1, so decrement in all cases.
+        tcpDetails.seq = tcpDetails.seq - 1;
+
+        return tcpDetails;
+    }
+
+    /**
+     * Switch the tcp socket out of repair mode.
+     *
+     * @param fd the fd of socket to switch back to normal.
+     */
+    private static void switchOutOfRepairMode(@NonNull final FileDescriptor fd) {
+        try {
+            Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_OFF);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "Cannot switch socket out of repair mode", e);
+            // Well, there is not much to do here to recover
+        }
+    }
+
+    /**
+     * Start monitoring incoming packets.
+     *
+     * @param fd socket fd to monitor.
+     * @param ki a {@link KeepaliveInfo} that tracks information about a socket keepalive.
+     * @param slot keepalive slot.
+     */
+    public void startSocketMonitor(@NonNull final FileDescriptor fd,
+            @NonNull final KeepaliveInfo ki, final int slot)
+            throws IllegalArgumentException, InvalidSocketException {
+        synchronized (mListeners) {
+            if (null != mListeners.get(slot)) {
+                throw new IllegalArgumentException("This slot is already taken");
+            }
+            for (int i = 0; i < mListeners.size(); ++i) {
+                if (fd.equals(mListeners.valueAt(i))) {
+                    Log.e(TAG, "This fd is already registered.");
+                    throw new InvalidSocketException(ERROR_INVALID_SOCKET);
+                }
+            }
+            mFdHandlerQueue.addOnFileDescriptorEventListener(fd, FD_EVENTS, (readyFd, events) -> {
+                // This can't be called twice because the queue guarantees that once the listener
+                // is unregistered it can't be called again, even for a message that arrived
+                // before it was unregistered.
+                final int reason;
+                if (0 != (events & EVENT_ERROR)) {
+                    reason = ERROR_INVALID_SOCKET;
+                } else {
+                    reason = DATA_RECEIVED;
+                }
+                ki.onFileDescriptorInitiatedStop(reason);
+                // The listener returns the new set of events to listen to. Because 0 means no
+                // event, the listener gets unregistered.
+                return 0;
+            });
+            mListeners.put(slot, fd);
+        }
+    }
+
+    /** Stop socket monitor */
+    // This slot may have been stopped automatically already because the socket received data,
+    // was closed on the other end or otherwise suffered some error. In this case, this function
+    // is a no-op.
+    public void stopSocketMonitor(final int slot) {
+        final FileDescriptor fd;
+        synchronized (mListeners) {
+            fd = mListeners.get(slot);
+            if (null == fd) return;
+            mListeners.remove(slot);
+        }
+        mFdHandlerQueue.removeOnFileDescriptorEventListener(fd);
+        if (DBG) Log.d(TAG, "Moving socket out of repair mode for stop : " + fd);
+        switchOutOfRepairMode(fd);
+    }
+
+    private static byte [] getAddress(InetSocketAddress inetAddr) {
+        return inetAddr.getAddress().getAddress();
+    }
+
+    private static int getPort(InetSocketAddress inetAddr) {
+        return inetAddr.getPort();
+    }
+
+    private static boolean isSocketIdle(FileDescriptor fd) throws ErrnoException {
+        return isReceiveQueueEmpty(fd) && isSendQueueEmpty(fd);
+    }
+
+    private static boolean isReceiveQueueEmpty(FileDescriptor fd)
+            throws ErrnoException {
+        final int result = OsCompat.ioctlInt(fd, SIOCINQ);
+        if (result != 0) {
+            Log.e(TAG, "Read queue has data");
+            return false;
+        }
+        return true;
+    }
+
+    private static boolean isSendQueueEmpty(FileDescriptor fd)
+            throws ErrnoException {
+        final int result = OsCompat.ioctlInt(fd, SIOCOUTQ);
+        if (result != 0) {
+            Log.e(TAG, "Write queue has data");
+            return false;
+        }
+        return true;
+    }
+
+    private static void dropAllIncomingPackets(FileDescriptor fd, boolean enable)
+            throws InvalidSocketException {
+        try {
+            if (enable) {
+                NetworkUtils.attachDropAllBPFFilter(fd);
+            } else {
+                NetworkUtils.detachBPFFilter(fd);
+            }
+        } catch (SocketException e) {
+            Log.e(TAG, "Socket Exception: ", e);
+            throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
new file mode 100644
index 0000000..657d5ec
--- /dev/null
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -0,0 +1,6047 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.MANAGE_NETWORK_POLICY;
+import static android.Manifest.permission.RECEIVE_DATA_ACTIVITY_CHANGE;
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE;
+import static android.net.ConnectivityManager.NetworkCallbackListener;
+import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
+import static android.net.ConnectivityManager.TYPE_DUMMY;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_MOBILE_MMS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
+import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
+import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA;
+import static android.net.ConnectivityManager.TYPE_MOBILE_IMS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_CBS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_IA;
+import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
+import static android.net.ConnectivityManager.TYPE_NONE;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivityManager.TYPE_WIMAX;
+import static android.net.ConnectivityManager.TYPE_PROXY;
+import static android.net.ConnectivityManager.getNetworkTypeName;
+import static android.net.ConnectivityManager.isNetworkTypeValid;
+import static android.net.NetworkPolicyManager.RULE_ALLOW_ALL;
+import static android.net.NetworkPolicyManager.RULE_REJECT_METERED;
+
+import android.app.AlarmManager;
+import android.app.AppOpsManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothTetheringDataTracker;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.net.CaptivePortalTracker;
+import android.net.ConnectivityManager;
+import android.net.DummyDataStateTracker;
+import android.net.IConnectivityManager;
+import android.net.INetworkManagementEventObserver;
+import android.net.INetworkPolicyListener;
+import android.net.INetworkPolicyManager;
+import android.net.INetworkStatsService;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.LinkProperties.CompareResult;
+import android.net.LinkQualityInfo;
+import android.net.MobileDataStateTracker;
+import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkCapabilities;
+import android.net.NetworkConfig;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkFactory;
+import android.net.NetworkQuotaInfo;
+import android.net.NetworkRequest;
+import android.net.NetworkState;
+import android.net.NetworkStateTracker;
+import android.net.NetworkUtils;
+import android.net.Proxy;
+import android.net.ProxyDataTracker;
+import android.net.ProxyInfo;
+import android.net.RouteInfo;
+import android.net.SamplingDataTracker;
+import android.net.Uri;
+import android.net.wimax.WimaxManagerConstants;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.Build;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.ParcelFileDescriptor;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.security.Credentials;
+import android.security.KeyStore;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.util.Xml;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.net.LegacyVpnInfo;
+import com.android.internal.net.VpnConfig;
+import com.android.internal.net.VpnProfile;
+import com.android.internal.telephony.DctConstants;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.XmlUtils;
+import com.android.server.am.BatteryStatsService;
+import com.android.server.connectivity.DataConnectionStats;
+import com.android.server.connectivity.Nat464Xlat;
+import com.android.server.connectivity.NetworkAgentInfo;
+import com.android.server.connectivity.NetworkMonitor;
+import com.android.server.connectivity.PacManager;
+import com.android.server.connectivity.Tethering;
+import com.android.server.connectivity.Vpn;
+import com.android.server.net.BaseNetworkObserver;
+import com.android.server.net.LockdownVpnTracker;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Sets;
+
+import dalvik.system.DexClassLoader;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.Constructor;
+import java.net.HttpURLConnection;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSession;
+
+import static android.net.ConnectivityManager.INVALID_NET_ID;
+
+/**
+ * @hide
+ */
+public class ConnectivityService extends IConnectivityManager.Stub {
+    private static final String TAG = "ConnectivityService";
+
+    private static final boolean DBG = true;
+    private static final boolean VDBG = true; // STOPSHIP
+
+    // network sampling debugging
+    private static final boolean SAMPLE_DBG = false;
+
+    private static final boolean LOGD_RULES = false;
+
+    // TODO: create better separation between radio types and network types
+
+    // how long to wait before switching back to a radio's default network
+    private static final int RESTORE_DEFAULT_NETWORK_DELAY = 1 * 60 * 1000;
+    // system property that can override the above value
+    private static final String NETWORK_RESTORE_DELAY_PROP_NAME =
+            "android.telephony.apn-restore";
+
+    // Default value if FAIL_FAST_TIME_MS is not set
+    private static final int DEFAULT_FAIL_FAST_TIME_MS = 1 * 60 * 1000;
+    // system property that can override DEFAULT_FAIL_FAST_TIME_MS
+    private static final String FAIL_FAST_TIME_MS =
+            "persist.radio.fail_fast_time_ms";
+
+    private static final String ACTION_PKT_CNT_SAMPLE_INTERVAL_ELAPSED =
+            "android.net.ConnectivityService.action.PKT_CNT_SAMPLE_INTERVAL_ELAPSED";
+
+    private static final int SAMPLE_INTERVAL_ELAPSED_REQUEST_CODE = 0;
+
+    private PendingIntent mSampleIntervalElapsedIntent;
+
+    // Set network sampling interval at 12 minutes, this way, even if the timers get
+    // aggregated, it will fire at around 15 minutes, which should allow us to
+    // aggregate this timer with other timers (specially the socket keep alive timers)
+    private static final int DEFAULT_SAMPLING_INTERVAL_IN_SECONDS = (SAMPLE_DBG ? 30 : 12 * 60);
+
+    // start network sampling a minute after booting ...
+    private static final int DEFAULT_START_SAMPLING_INTERVAL_IN_SECONDS = (SAMPLE_DBG ? 30 : 60);
+
+    AlarmManager mAlarmManager;
+
+    // used in recursive route setting to add gateways for the host for which
+    // a host route was requested.
+    private static final int MAX_HOSTROUTE_CYCLE_COUNT = 10;
+
+    private Tethering mTethering;
+
+    private KeyStore mKeyStore;
+
+    @GuardedBy("mVpns")
+    private final SparseArray<Vpn> mVpns = new SparseArray<Vpn>();
+    private VpnCallback mVpnCallback = new VpnCallback();
+
+    private boolean mLockdownEnabled;
+    private LockdownVpnTracker mLockdownTracker;
+
+    private Nat464Xlat mClat;
+
+    /** Lock around {@link #mUidRules} and {@link #mMeteredIfaces}. */
+    private Object mRulesLock = new Object();
+    /** Currently active network rules by UID. */
+    private SparseIntArray mUidRules = new SparseIntArray();
+    /** Set of ifaces that are costly. */
+    private HashSet<String> mMeteredIfaces = Sets.newHashSet();
+
+    /**
+     * Sometimes we want to refer to the individual network state
+     * trackers separately, and sometimes we just want to treat them
+     * abstractly.
+     */
+    private NetworkStateTracker mNetTrackers[];
+
+    /* Handles captive portal check on a network */
+    private CaptivePortalTracker mCaptivePortalTracker;
+
+    /**
+     * The link properties that define the current links
+     */
+    private LinkProperties mCurrentLinkProperties[];
+
+    /**
+     * A per Net list of the PID's that requested access to the net
+     * used both as a refcount and for per-PID DNS selection
+     */
+    private List<Integer> mNetRequestersPids[];
+
+    // priority order of the nettrackers
+    // (excluding dynamically set mNetworkPreference)
+    // TODO - move mNetworkTypePreference into this
+    private int[] mPriorityList;
+
+    private Context mContext;
+    private int mNetworkPreference;
+    private int mActiveDefaultNetwork = -1;
+    // 0 is full bad, 100 is full good
+    private int mDefaultInetCondition = 0;
+    private int mDefaultInetConditionPublished = 0;
+    private boolean mInetConditionChangeInFlight = false;
+    private int mDefaultConnectionSequence = 0;
+
+    private Object mDnsLock = new Object();
+    private int mNumDnsEntries;
+
+    private boolean mTestMode;
+    private static ConnectivityService sServiceInstance;
+
+    private INetworkManagementService mNetd;
+    private INetworkPolicyManager mPolicyManager;
+
+    private static final int ENABLED  = 1;
+    private static final int DISABLED = 0;
+
+    private static final boolean ADD = true;
+    private static final boolean REMOVE = false;
+
+    private static final boolean TO_DEFAULT_TABLE = true;
+    private static final boolean TO_SECONDARY_TABLE = false;
+
+    private static final boolean EXEMPT = true;
+    private static final boolean UNEXEMPT = false;
+
+    /**
+     * used internally as a delayed event to make us switch back to the
+     * default network
+     */
+    private static final int EVENT_RESTORE_DEFAULT_NETWORK = 1;
+
+    /**
+     * used internally to change our mobile data enabled flag
+     */
+    private static final int EVENT_CHANGE_MOBILE_DATA_ENABLED = 2;
+
+    /**
+     * used internally to synchronize inet condition reports
+     * arg1 = networkType
+     * arg2 = condition (0 bad, 100 good)
+     */
+    private static final int EVENT_INET_CONDITION_CHANGE = 4;
+
+    /**
+     * used internally to mark the end of inet condition hold periods
+     * arg1 = networkType
+     */
+    private static final int EVENT_INET_CONDITION_HOLD_END = 5;
+
+    /**
+     * used internally to clear a wakelock when transitioning
+     * from one net to another
+     */
+    private static final int EVENT_CLEAR_NET_TRANSITION_WAKELOCK = 8;
+
+    /**
+     * used internally to reload global proxy settings
+     */
+    private static final int EVENT_APPLY_GLOBAL_HTTP_PROXY = 9;
+
+    /**
+     * used internally to set external dependency met/unmet
+     * arg1 = ENABLED (met) or DISABLED (unmet)
+     * arg2 = NetworkType
+     */
+    private static final int EVENT_SET_DEPENDENCY_MET = 10;
+
+    /**
+     * used internally to send a sticky broadcast delayed.
+     */
+    private static final int EVENT_SEND_STICKY_BROADCAST_INTENT = 11;
+
+    /**
+     * Used internally to
+     * {@link NetworkStateTracker#setPolicyDataEnable(boolean)}.
+     */
+    private static final int EVENT_SET_POLICY_DATA_ENABLE = 12;
+
+    private static final int EVENT_VPN_STATE_CHANGED = 13;
+
+    /**
+     * Used internally to disable fail fast of mobile data
+     */
+    private static final int EVENT_ENABLE_FAIL_FAST_MOBILE_DATA = 14;
+
+    /**
+     * used internally to indicate that data sampling interval is up
+     */
+    private static final int EVENT_SAMPLE_INTERVAL_ELAPSED = 15;
+
+    /**
+     * PAC manager has received new port.
+     */
+    private static final int EVENT_PROXY_HAS_CHANGED = 16;
+
+    /**
+     * used internally when registering NetworkFactories
+     * obj = NetworkFactoryInfo
+     */
+    private static final int EVENT_REGISTER_NETWORK_FACTORY = 17;
+
+    /**
+     * used internally when registering NetworkAgents
+     * obj = Messenger
+     */
+    private static final int EVENT_REGISTER_NETWORK_AGENT = 18;
+
+    /**
+     * used to add a network request
+     * includes a NetworkRequestInfo
+     */
+    private static final int EVENT_REGISTER_NETWORK_REQUEST = 19;
+
+    /**
+     * indicates a timeout period is over - check if we had a network yet or not
+     * and if not, call the timeout calback (but leave the request live until they
+     * cancel it.
+     * includes a NetworkRequestInfo
+     */
+    private static final int EVENT_TIMEOUT_NETWORK_REQUEST = 20;
+
+    /**
+     * used to add a network listener - no request
+     * includes a NetworkRequestInfo
+     */
+    private static final int EVENT_REGISTER_NETWORK_LISTENER = 21;
+
+    /**
+     * used to remove a network request, either a listener or a real request
+     * includes a NetworkRequest
+     */
+    private static final int EVENT_RELEASE_NETWORK_REQUEST = 22;
+
+    /**
+     * used internally when registering NetworkFactories
+     * obj = Messenger
+     */
+    private static final int EVENT_UNREGISTER_NETWORK_FACTORY = 23;
+
+
+    /** Handler used for internal events. */
+    final private InternalHandler mHandler;
+    /** Handler used for incoming {@link NetworkStateTracker} events. */
+    final private NetworkStateTrackerHandler mTrackerHandler;
+
+    // list of DeathRecipients used to make sure features are turned off when
+    // a process dies
+    private List<FeatureUser> mFeatureUsers;
+
+    private boolean mSystemReady;
+    private Intent mInitialBroadcast;
+
+    private PowerManager.WakeLock mNetTransitionWakeLock;
+    private String mNetTransitionWakeLockCausedBy = "";
+    private int mNetTransitionWakeLockSerialNumber;
+    private int mNetTransitionWakeLockTimeout;
+
+    private InetAddress mDefaultDns;
+
+    // Lock for protecting access to mAddedRoutes and mExemptAddresses
+    private final Object mRoutesLock = new Object();
+
+    // this collection is used to refcount the added routes - if there are none left
+    // it's time to remove the route from the route table
+    @GuardedBy("mRoutesLock")
+    private Collection<RouteInfo> mAddedRoutes = new ArrayList<RouteInfo>();
+
+    // this collection corresponds to the entries of mAddedRoutes that have routing exemptions
+    // used to handle cleanup of exempt rules
+    @GuardedBy("mRoutesLock")
+    private Collection<LinkAddress> mExemptAddresses = new ArrayList<LinkAddress>();
+
+    // used in DBG mode to track inet condition reports
+    private static final int INET_CONDITION_LOG_MAX_SIZE = 15;
+    private ArrayList mInetLog;
+
+    // track the current default http proxy - tell the world if we get a new one (real change)
+    private ProxyInfo mDefaultProxy = null;
+    private Object mProxyLock = new Object();
+    private boolean mDefaultProxyDisabled = false;
+
+    // track the global proxy.
+    private ProxyInfo mGlobalProxy = null;
+
+    private PacManager mPacManager = null;
+
+    private SettingsObserver mSettingsObserver;
+
+    private AppOpsManager mAppOpsManager;
+
+    NetworkConfig[] mNetConfigs;
+    int mNetworksDefined;
+
+    private static class RadioAttributes {
+        public int mSimultaneity;
+        public int mType;
+        public RadioAttributes(String init) {
+            String fragments[] = init.split(",");
+            mType = Integer.parseInt(fragments[0]);
+            mSimultaneity = Integer.parseInt(fragments[1]);
+        }
+    }
+    RadioAttributes[] mRadioAttributes;
+
+    // the set of network types that can only be enabled by system/sig apps
+    List mProtectedNetworks;
+
+    private DataConnectionStats mDataConnectionStats;
+
+    private AtomicInteger mEnableFailFastMobileDataTag = new AtomicInteger(0);
+
+    TelephonyManager mTelephonyManager;
+
+    // sequence number for Networks
+    private final static int MIN_NET_ID = 10; // some reserved marks
+    private final static int MAX_NET_ID = 65535;
+    private int mNextNetId = MIN_NET_ID;
+
+    // sequence number of NetworkRequests
+    private int mNextNetworkRequestId = 1;
+
+    private static final int UID_UNUSED = -1;
+
+    /**
+     * Implements support for the legacy "one network per network type" model.
+     *
+     * We used to have a static array of NetworkStateTrackers, one for each
+     * network type, but that doesn't work any more now that we can have,
+     * for example, more that one wifi network. This class stores all the
+     * NetworkAgentInfo objects that support a given type, but the legacy
+     * API will only see the first one.
+     *
+     * It serves two main purposes:
+     *
+     * 1. Provide information about "the network for a given type" (since this
+     *    API only supports one).
+     * 2. Send legacy connectivity change broadcasts. Broadcasts are sent if
+     *    the first network for a given type changes, or if the default network
+     *    changes.
+     */
+    private class LegacyTypeTracker {
+        /**
+         * Array of lists, one per legacy network type (e.g., TYPE_MOBILE_MMS).
+         * Each list holds references to all NetworkAgentInfos that are used to
+         * satisfy requests for that network type.
+         *
+         * This array is built out at startup such that an unsupported network
+         * doesn't get an ArrayList instance, making this a tristate:
+         * unsupported, supported but not active and active.
+         *
+         * The actual lists are populated when we scan the network types that
+         * are supported on this device.
+         */
+        private ArrayList<NetworkAgentInfo> mTypeLists[];
+
+        public LegacyTypeTracker() {
+            mTypeLists = (ArrayList<NetworkAgentInfo>[])
+                    new ArrayList[ConnectivityManager.MAX_NETWORK_TYPE + 1];
+        }
+
+        public void addSupportedType(int type) {
+            if (mTypeLists[type] != null) {
+                throw new IllegalStateException(
+                        "legacy list for type " + type + "already initialized");
+            }
+            mTypeLists[type] = new ArrayList<NetworkAgentInfo>();
+        }
+
+        private boolean isDefaultNetwork(NetworkAgentInfo nai) {
+            return mNetworkForRequestId.get(mDefaultRequest.requestId) == nai;
+        }
+
+        public boolean isTypeSupported(int type) {
+            return isNetworkTypeValid(type) && mTypeLists[type] != null;
+        }
+
+        public NetworkAgentInfo getNetworkForType(int type) {
+            if (isTypeSupported(type) && !mTypeLists[type].isEmpty()) {
+                return mTypeLists[type].get(0);
+            } else {
+                return null;
+            }
+        }
+
+        public void add(int type, NetworkAgentInfo nai) {
+            if (!isTypeSupported(type)) {
+                return;  // Invalid network type.
+            }
+            if (VDBG) log("Adding agent " + nai + " for legacy network type " + type);
+
+            ArrayList<NetworkAgentInfo> list = mTypeLists[type];
+            if (list.contains(nai)) {
+                loge("Attempting to register duplicate agent for type " + type + ": " + nai);
+                return;
+            }
+
+            if (list.isEmpty() || isDefaultNetwork(nai)) {
+                if (VDBG) log("Sending connected broadcast for type " + type +
+                              "isDefaultNetwork=" + isDefaultNetwork(nai));
+                sendLegacyNetworkBroadcast(nai, true, type);
+            }
+            list.add(nai);
+        }
+
+        public void remove(NetworkAgentInfo nai) {
+            if (VDBG) log("Removing agent " + nai);
+            for (int type = 0; type < mTypeLists.length; type++) {
+                ArrayList<NetworkAgentInfo> list = mTypeLists[type];
+                if (list == null || list.isEmpty()) {
+                    continue;
+                }
+
+                boolean wasFirstNetwork = false;
+                if (list.get(0).equals(nai)) {
+                    // This network was the first in the list. Send broadcast.
+                    wasFirstNetwork = true;
+                }
+                list.remove(nai);
+
+                if (wasFirstNetwork || isDefaultNetwork(nai)) {
+                    if (VDBG) log("Sending disconnected broadcast for type " + type +
+                                  "isDefaultNetwork=" + isDefaultNetwork(nai));
+                    sendLegacyNetworkBroadcast(nai, false, type);
+                }
+
+                if (!list.isEmpty() && wasFirstNetwork) {
+                    if (VDBG) log("Other network available for type " + type +
+                                  ", sending connected broadcast");
+                    sendLegacyNetworkBroadcast(list.get(0), false, type);
+                }
+            }
+        }
+    }
+    private LegacyTypeTracker mLegacyTypeTracker = new LegacyTypeTracker();
+
+    public ConnectivityService(Context context, INetworkManagementService netd,
+            INetworkStatsService statsService, INetworkPolicyManager policyManager) {
+        // Currently, omitting a NetworkFactory will create one internally
+        // TODO: create here when we have cleaner WiMAX support
+        this(context, netd, statsService, policyManager, null);
+    }
+
+    public ConnectivityService(Context context, INetworkManagementService netManager,
+            INetworkStatsService statsService, INetworkPolicyManager policyManager,
+            NetworkFactory netFactory) {
+        if (DBG) log("ConnectivityService starting up");
+
+        NetworkCapabilities netCap = new NetworkCapabilities();
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        netCap.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+        mDefaultRequest = new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId());
+        NetworkRequestInfo nri = new NetworkRequestInfo(null, mDefaultRequest, new Binder(),
+                NetworkRequestInfo.REQUEST);
+        mNetworkRequests.put(mDefaultRequest, nri);
+
+        HandlerThread handlerThread = new HandlerThread("ConnectivityServiceThread");
+        handlerThread.start();
+        mHandler = new InternalHandler(handlerThread.getLooper());
+        mTrackerHandler = new NetworkStateTrackerHandler(handlerThread.getLooper());
+
+        if (netFactory == null) {
+            netFactory = new DefaultNetworkFactory(context, mTrackerHandler);
+        }
+
+        // setup our unique device name
+        if (TextUtils.isEmpty(SystemProperties.get("net.hostname"))) {
+            String id = Settings.Secure.getString(context.getContentResolver(),
+                    Settings.Secure.ANDROID_ID);
+            if (id != null && id.length() > 0) {
+                String name = new String("android-").concat(id);
+                SystemProperties.set("net.hostname", name);
+            }
+        }
+
+        // read our default dns server ip
+        String dns = Settings.Global.getString(context.getContentResolver(),
+                Settings.Global.DEFAULT_DNS_SERVER);
+        if (dns == null || dns.length() == 0) {
+            dns = context.getResources().getString(
+                    com.android.internal.R.string.config_default_dns_server);
+        }
+        try {
+            mDefaultDns = NetworkUtils.numericToInetAddress(dns);
+        } catch (IllegalArgumentException e) {
+            loge("Error setting defaultDns using " + dns);
+        }
+
+        mContext = checkNotNull(context, "missing Context");
+        mNetd = checkNotNull(netManager, "missing INetworkManagementService");
+        mPolicyManager = checkNotNull(policyManager, "missing INetworkPolicyManager");
+        mKeyStore = KeyStore.getInstance();
+        mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+
+        try {
+            mPolicyManager.registerListener(mPolicyListener);
+        } catch (RemoteException e) {
+            // ouch, no rules updates means some processes may never get network
+            loge("unable to register INetworkPolicyListener" + e.toString());
+        }
+
+        final PowerManager powerManager = (PowerManager) context.getSystemService(
+                Context.POWER_SERVICE);
+        mNetTransitionWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+        mNetTransitionWakeLockTimeout = mContext.getResources().getInteger(
+                com.android.internal.R.integer.config_networkTransitionTimeout);
+
+        mNetTrackers = new NetworkStateTracker[
+                ConnectivityManager.MAX_NETWORK_TYPE+1];
+        mCurrentLinkProperties = new LinkProperties[ConnectivityManager.MAX_NETWORK_TYPE+1];
+
+        mRadioAttributes = new RadioAttributes[ConnectivityManager.MAX_RADIO_TYPE+1];
+        mNetConfigs = new NetworkConfig[ConnectivityManager.MAX_NETWORK_TYPE+1];
+
+        // Load device network attributes from resources
+        String[] raStrings = context.getResources().getStringArray(
+                com.android.internal.R.array.radioAttributes);
+        for (String raString : raStrings) {
+            RadioAttributes r = new RadioAttributes(raString);
+            if (VDBG) log("raString=" + raString + " r=" + r);
+            if (r.mType > ConnectivityManager.MAX_RADIO_TYPE) {
+                loge("Error in radioAttributes - ignoring attempt to define type " + r.mType);
+                continue;
+            }
+            if (mRadioAttributes[r.mType] != null) {
+                loge("Error in radioAttributes - ignoring attempt to redefine type " +
+                        r.mType);
+                continue;
+            }
+            mRadioAttributes[r.mType] = r;
+        }
+
+        // TODO: What is the "correct" way to do determine if this is a wifi only device?
+        boolean wifiOnly = SystemProperties.getBoolean("ro.radio.noril", false);
+        log("wifiOnly=" + wifiOnly);
+        String[] naStrings = context.getResources().getStringArray(
+                com.android.internal.R.array.networkAttributes);
+        for (String naString : naStrings) {
+            try {
+                NetworkConfig n = new NetworkConfig(naString);
+                if (VDBG) log("naString=" + naString + " config=" + n);
+                if (n.type > ConnectivityManager.MAX_NETWORK_TYPE) {
+                    loge("Error in networkAttributes - ignoring attempt to define type " +
+                            n.type);
+                    continue;
+                }
+                if (wifiOnly && ConnectivityManager.isNetworkTypeMobile(n.type)) {
+                    log("networkAttributes - ignoring mobile as this dev is wifiOnly " +
+                            n.type);
+                    continue;
+                }
+                if (mNetConfigs[n.type] != null) {
+                    loge("Error in networkAttributes - ignoring attempt to redefine type " +
+                            n.type);
+                    continue;
+                }
+                if (mRadioAttributes[n.radio] == null) {
+                    loge("Error in networkAttributes - ignoring attempt to use undefined " +
+                            "radio " + n.radio + " in network type " + n.type);
+                    continue;
+                }
+                mLegacyTypeTracker.addSupportedType(n.type);
+
+                mNetConfigs[n.type] = n;
+                mNetworksDefined++;
+            } catch(Exception e) {
+                // ignore it - leave the entry null
+            }
+        }
+        if (VDBG) log("mNetworksDefined=" + mNetworksDefined);
+
+        mProtectedNetworks = new ArrayList<Integer>();
+        int[] protectedNetworks = context.getResources().getIntArray(
+                com.android.internal.R.array.config_protectedNetworks);
+        for (int p : protectedNetworks) {
+            if ((mNetConfigs[p] != null) && (mProtectedNetworks.contains(p) == false)) {
+                mProtectedNetworks.add(p);
+            } else {
+                if (DBG) loge("Ignoring protectedNetwork " + p);
+            }
+        }
+
+        // high priority first
+        mPriorityList = new int[mNetworksDefined];
+        {
+            int insertionPoint = mNetworksDefined-1;
+            int currentLowest = 0;
+            int nextLowest = 0;
+            while (insertionPoint > -1) {
+                for (NetworkConfig na : mNetConfigs) {
+                    if (na == null) continue;
+                    if (na.priority < currentLowest) continue;
+                    if (na.priority > currentLowest) {
+                        if (na.priority < nextLowest || nextLowest == 0) {
+                            nextLowest = na.priority;
+                        }
+                        continue;
+                    }
+                    mPriorityList[insertionPoint--] = na.type;
+                }
+                currentLowest = nextLowest;
+                nextLowest = 0;
+            }
+        }
+
+        mNetRequestersPids =
+                (List<Integer> [])new ArrayList[ConnectivityManager.MAX_NETWORK_TYPE+1];
+        for (int i : mPriorityList) {
+            mNetRequestersPids[i] = new ArrayList<Integer>();
+        }
+
+        mFeatureUsers = new ArrayList<FeatureUser>();
+
+        mTestMode = SystemProperties.get("cm.test.mode").equals("true")
+                && SystemProperties.get("ro.build.type").equals("eng");
+
+        // Create and start trackers for hard-coded networks
+        for (int targetNetworkType : mPriorityList) {
+            final NetworkConfig config = mNetConfigs[targetNetworkType];
+            final NetworkStateTracker tracker;
+            try {
+                tracker = netFactory.createTracker(targetNetworkType, config);
+                mNetTrackers[targetNetworkType] = tracker;
+            } catch (IllegalArgumentException e) {
+                Slog.e(TAG, "Problem creating " + getNetworkTypeName(targetNetworkType)
+                        + " tracker: " + e);
+                continue;
+            }
+
+            tracker.startMonitoring(context, mTrackerHandler);
+            if (config.isDefault()) {
+                tracker.reconnect();
+            }
+        }
+
+        mTethering = new Tethering(mContext, mNetd, statsService, this, mHandler.getLooper());
+
+        //set up the listener for user state for creating user VPNs
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_USER_STARTING);
+        intentFilter.addAction(Intent.ACTION_USER_STOPPING);
+        mContext.registerReceiverAsUser(
+                mUserIntentReceiver, UserHandle.ALL, intentFilter, null, null);
+        mClat = new Nat464Xlat(mContext, mNetd, this, mTrackerHandler);
+
+        try {
+            mNetd.registerObserver(mTethering);
+            mNetd.registerObserver(mDataActivityObserver);
+            mNetd.registerObserver(mClat);
+        } catch (RemoteException e) {
+            loge("Error registering observer :" + e);
+        }
+
+        if (DBG) {
+            mInetLog = new ArrayList();
+        }
+
+        mSettingsObserver = new SettingsObserver(mHandler, EVENT_APPLY_GLOBAL_HTTP_PROXY);
+        mSettingsObserver.observe(mContext);
+
+        mDataConnectionStats = new DataConnectionStats(mContext);
+        mDataConnectionStats.startMonitoring();
+
+        // start network sampling ..
+        Intent intent = new Intent(ACTION_PKT_CNT_SAMPLE_INTERVAL_ELAPSED, null);
+        mSampleIntervalElapsedIntent = PendingIntent.getBroadcast(mContext,
+                SAMPLE_INTERVAL_ELAPSED_REQUEST_CODE, intent, 0);
+
+        mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
+        setAlarm(DEFAULT_START_SAMPLING_INTERVAL_IN_SECONDS * 1000, mSampleIntervalElapsedIntent);
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(ACTION_PKT_CNT_SAMPLE_INTERVAL_ELAPSED);
+        mContext.registerReceiver(
+                new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        String action = intent.getAction();
+                        if (action.equals(ACTION_PKT_CNT_SAMPLE_INTERVAL_ELAPSED)) {
+                            mHandler.sendMessage(mHandler.obtainMessage
+                                    (EVENT_SAMPLE_INTERVAL_ELAPSED));
+                        }
+                    }
+                },
+                new IntentFilter(filter));
+
+        mPacManager = new PacManager(mContext, mHandler, EVENT_PROXY_HAS_CHANGED);
+
+        filter = new IntentFilter();
+        filter.addAction(CONNECTED_TO_PROVISIONING_NETWORK_ACTION);
+        mContext.registerReceiver(mProvisioningReceiver, filter);
+
+        mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+    }
+
+    private synchronized int nextNetworkRequestId() {
+        return mNextNetworkRequestId++;
+    }
+
+    private synchronized int nextNetId() {
+        int netId = mNextNetId;
+        if (++mNextNetId > MAX_NET_ID) mNextNetId = MIN_NET_ID;
+        return netId;
+    }
+
+    /**
+     * Factory that creates {@link NetworkStateTracker} instances using given
+     * {@link NetworkConfig}.
+     *
+     * TODO - this is obsolete and will be deleted.  It's replaced by the
+     * registerNetworkFactory call and protocol.
+     * @Deprecated in favor of registerNetworkFactory dynamic bindings
+     */
+    public interface NetworkFactory {
+        public NetworkStateTracker createTracker(int targetNetworkType, NetworkConfig config);
+    }
+
+    private static class DefaultNetworkFactory implements NetworkFactory {
+        private final Context mContext;
+        private final Handler mTrackerHandler;
+
+        public DefaultNetworkFactory(Context context, Handler trackerHandler) {
+            mContext = context;
+            mTrackerHandler = trackerHandler;
+        }
+
+        @Override
+        public NetworkStateTracker createTracker(int targetNetworkType, NetworkConfig config) {
+            switch (config.radio) {
+                case TYPE_DUMMY:
+                    return new DummyDataStateTracker(targetNetworkType, config.name);
+                case TYPE_BLUETOOTH:
+                    return BluetoothTetheringDataTracker.getInstance();
+                case TYPE_WIMAX:
+                    return makeWimaxStateTracker(mContext, mTrackerHandler);
+                case TYPE_PROXY:
+                    return new ProxyDataTracker();
+                default:
+                    throw new IllegalArgumentException(
+                            "Trying to create a NetworkStateTracker for an unknown radio type: "
+                            + config.radio);
+            }
+        }
+    }
+
+    /**
+     * Loads external WiMAX library and registers as system service, returning a
+     * {@link NetworkStateTracker} for WiMAX. Caller is still responsible for
+     * invoking {@link NetworkStateTracker#startMonitoring(Context, Handler)}.
+     */
+    private static NetworkStateTracker makeWimaxStateTracker(
+            Context context, Handler trackerHandler) {
+        // Initialize Wimax
+        DexClassLoader wimaxClassLoader;
+        Class wimaxStateTrackerClass = null;
+        Class wimaxServiceClass = null;
+        Class wimaxManagerClass;
+        String wimaxJarLocation;
+        String wimaxLibLocation;
+        String wimaxManagerClassName;
+        String wimaxServiceClassName;
+        String wimaxStateTrackerClassName;
+
+        NetworkStateTracker wimaxStateTracker = null;
+
+        boolean isWimaxEnabled = context.getResources().getBoolean(
+                com.android.internal.R.bool.config_wimaxEnabled);
+
+        if (isWimaxEnabled) {
+            try {
+                wimaxJarLocation = context.getResources().getString(
+                        com.android.internal.R.string.config_wimaxServiceJarLocation);
+                wimaxLibLocation = context.getResources().getString(
+                        com.android.internal.R.string.config_wimaxNativeLibLocation);
+                wimaxManagerClassName = context.getResources().getString(
+                        com.android.internal.R.string.config_wimaxManagerClassname);
+                wimaxServiceClassName = context.getResources().getString(
+                        com.android.internal.R.string.config_wimaxServiceClassname);
+                wimaxStateTrackerClassName = context.getResources().getString(
+                        com.android.internal.R.string.config_wimaxStateTrackerClassname);
+
+                if (DBG) log("wimaxJarLocation: " + wimaxJarLocation);
+                wimaxClassLoader =  new DexClassLoader(wimaxJarLocation,
+                        new ContextWrapper(context).getCacheDir().getAbsolutePath(),
+                        wimaxLibLocation, ClassLoader.getSystemClassLoader());
+
+                try {
+                    wimaxManagerClass = wimaxClassLoader.loadClass(wimaxManagerClassName);
+                    wimaxStateTrackerClass = wimaxClassLoader.loadClass(wimaxStateTrackerClassName);
+                    wimaxServiceClass = wimaxClassLoader.loadClass(wimaxServiceClassName);
+                } catch (ClassNotFoundException ex) {
+                    loge("Exception finding Wimax classes: " + ex.toString());
+                    return null;
+                }
+            } catch(Resources.NotFoundException ex) {
+                loge("Wimax Resources does not exist!!! ");
+                return null;
+            }
+
+            try {
+                if (DBG) log("Starting Wimax Service... ");
+
+                Constructor wmxStTrkrConst = wimaxStateTrackerClass.getConstructor
+                        (new Class[] {Context.class, Handler.class});
+                wimaxStateTracker = (NetworkStateTracker) wmxStTrkrConst.newInstance(
+                        context, trackerHandler);
+
+                Constructor wmxSrvConst = wimaxServiceClass.getDeclaredConstructor
+                        (new Class[] {Context.class, wimaxStateTrackerClass});
+                wmxSrvConst.setAccessible(true);
+                IBinder svcInvoker = (IBinder)wmxSrvConst.newInstance(context, wimaxStateTracker);
+                wmxSrvConst.setAccessible(false);
+
+                ServiceManager.addService(WimaxManagerConstants.WIMAX_SERVICE, svcInvoker);
+
+            } catch(Exception ex) {
+                loge("Exception creating Wimax classes: " + ex.toString());
+                return null;
+            }
+        } else {
+            loge("Wimax is not enabled or not added to the network attributes!!! ");
+            return null;
+        }
+
+        return wimaxStateTracker;
+    }
+
+    private int getConnectivityChangeDelay() {
+        final ContentResolver cr = mContext.getContentResolver();
+
+        /** Check system properties for the default value then use secure settings value, if any. */
+        int defaultDelay = SystemProperties.getInt(
+                "conn." + Settings.Global.CONNECTIVITY_CHANGE_DELAY,
+                ConnectivityManager.CONNECTIVITY_CHANGE_DELAY_DEFAULT);
+        return Settings.Global.getInt(cr, Settings.Global.CONNECTIVITY_CHANGE_DELAY,
+                defaultDelay);
+    }
+
+    private boolean teardown(NetworkStateTracker netTracker) {
+        if (netTracker.teardown()) {
+            netTracker.setTeardownRequested(true);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Check if UID should be blocked from using the network represented by the
+     * given {@link NetworkStateTracker}.
+     */
+    private boolean isNetworkBlocked(int networkType, int uid) {
+        final boolean networkCostly;
+        final int uidRules;
+
+        LinkProperties lp = getLinkPropertiesForType(networkType);
+        final String iface = (lp == null ? "" : lp.getInterfaceName());
+        synchronized (mRulesLock) {
+            networkCostly = mMeteredIfaces.contains(iface);
+            uidRules = mUidRules.get(uid, RULE_ALLOW_ALL);
+        }
+
+        if (networkCostly && (uidRules & RULE_REJECT_METERED) != 0) {
+            return true;
+        }
+
+        // no restrictive rules; network is visible
+        return false;
+    }
+
+    /**
+     * Return a filtered {@link NetworkInfo}, potentially marked
+     * {@link DetailedState#BLOCKED} based on
+     * {@link #isNetworkBlocked}.
+     */
+    private NetworkInfo getFilteredNetworkInfo(int networkType, int uid) {
+        NetworkInfo info = getNetworkInfoForType(networkType);
+        if (isNetworkBlocked(networkType, uid)) {
+            // network is blocked; clone and override state
+            info = new NetworkInfo(info);
+            info.setDetailedState(DetailedState.BLOCKED, null, null);
+        }
+        if (mLockdownTracker != null) {
+            info = mLockdownTracker.augmentNetworkInfo(info);
+        }
+        return info;
+    }
+
+    /**
+     * Return NetworkInfo for the active (i.e., connected) network interface.
+     * It is assumed that at most one network is active at a time. If more
+     * than one is active, it is indeterminate which will be returned.
+     * @return the info for the active network, or {@code null} if none is
+     * active
+     */
+    @Override
+    public NetworkInfo getActiveNetworkInfo() {
+        enforceAccessPermission();
+        final int uid = Binder.getCallingUid();
+        return getNetworkInfo(mActiveDefaultNetwork, uid);
+    }
+
+    // only called when the default request is satisfied
+    private void updateActiveDefaultNetwork(NetworkAgentInfo nai) {
+        if (nai != null) {
+            mActiveDefaultNetwork = nai.networkInfo.getType();
+        } else {
+            mActiveDefaultNetwork = TYPE_NONE;
+        }
+    }
+
+    /**
+     * Find the first Provisioning network.
+     *
+     * @return NetworkInfo or null if none.
+     */
+    private NetworkInfo getProvisioningNetworkInfo() {
+        enforceAccessPermission();
+
+        // Find the first Provisioning Network
+        NetworkInfo provNi = null;
+        for (NetworkInfo ni : getAllNetworkInfo()) {
+            if (ni.isConnectedToProvisioningNetwork()) {
+                provNi = ni;
+                break;
+            }
+        }
+        if (DBG) log("getProvisioningNetworkInfo: X provNi=" + provNi);
+        return provNi;
+    }
+
+    /**
+     * Find the first Provisioning network or the ActiveDefaultNetwork
+     * if there is no Provisioning network
+     *
+     * @return NetworkInfo or null if none.
+     */
+    @Override
+    public NetworkInfo getProvisioningOrActiveNetworkInfo() {
+        enforceAccessPermission();
+
+        NetworkInfo provNi = getProvisioningNetworkInfo();
+        if (provNi == null) {
+            final int uid = Binder.getCallingUid();
+            provNi = getNetworkInfo(mActiveDefaultNetwork, uid);
+        }
+        if (DBG) log("getProvisioningOrActiveNetworkInfo: X provNi=" + provNi);
+        return provNi;
+    }
+
+    public NetworkInfo getActiveNetworkInfoUnfiltered() {
+        enforceAccessPermission();
+        if (isNetworkTypeValid(mActiveDefaultNetwork)) {
+            return getNetworkInfoForType(mActiveDefaultNetwork);
+        }
+        return null;
+    }
+
+    @Override
+    public NetworkInfo getActiveNetworkInfoForUid(int uid) {
+        enforceConnectivityInternalPermission();
+        return getNetworkInfo(mActiveDefaultNetwork, uid);
+    }
+
+    @Override
+    public NetworkInfo getNetworkInfo(int networkType) {
+        enforceAccessPermission();
+        final int uid = Binder.getCallingUid();
+        return getNetworkInfo(networkType, uid);
+    }
+
+    private NetworkInfo getNetworkInfo(int networkType, int uid) {
+        NetworkInfo info = null;
+        if (isNetworkTypeValid(networkType)) {
+            if (getNetworkInfoForType(networkType) != null) {
+                info = getFilteredNetworkInfo(networkType, uid);
+            }
+        }
+        return info;
+    }
+
+    @Override
+    public NetworkInfo[] getAllNetworkInfo() {
+        enforceAccessPermission();
+        final int uid = Binder.getCallingUid();
+        final ArrayList<NetworkInfo> result = Lists.newArrayList();
+        synchronized (mRulesLock) {
+            for (int networkType = 0; networkType <= ConnectivityManager.MAX_NETWORK_TYPE;
+                    networkType++) {
+                if (getNetworkInfoForType(networkType) != null) {
+                    result.add(getFilteredNetworkInfo(networkType, uid));
+                }
+            }
+        }
+        return result.toArray(new NetworkInfo[result.size()]);
+    }
+
+    @Override
+    public boolean isNetworkSupported(int networkType) {
+        enforceAccessPermission();
+        return (isNetworkTypeValid(networkType) && (getNetworkInfoForType(networkType) != null));
+    }
+
+    /**
+     * Return LinkProperties for the active (i.e., connected) default
+     * network interface.  It is assumed that at most one default network
+     * is active at a time. If more than one is active, it is indeterminate
+     * which will be returned.
+     * @return the ip properties for the active network, or {@code null} if
+     * none is active
+     */
+    @Override
+    public LinkProperties getActiveLinkProperties() {
+        return getLinkPropertiesForType(mActiveDefaultNetwork);
+    }
+
+    @Override
+    public LinkProperties getLinkPropertiesForType(int networkType) {
+        enforceAccessPermission();
+        if (isNetworkTypeValid(networkType)) {
+            return getLinkPropertiesForTypeInternal(networkType);
+        }
+        return null;
+    }
+
+    // TODO - this should be ALL networks
+    @Override
+    public LinkProperties getLinkProperties(Network network) {
+        enforceAccessPermission();
+        NetworkAgentInfo nai = mNetworkForNetId.get(network.netId);
+        if (nai != null) return new LinkProperties(nai.linkProperties);
+        return null;
+    }
+
+    @Override
+    public NetworkCapabilities getNetworkCapabilities(Network network) {
+        enforceAccessPermission();
+        NetworkAgentInfo nai = mNetworkForNetId.get(network.netId);
+        if (nai != null) return new NetworkCapabilities(nai.networkCapabilities);
+        return null;
+    }
+
+    @Override
+    public NetworkState[] getAllNetworkState() {
+        enforceAccessPermission();
+        final int uid = Binder.getCallingUid();
+        final ArrayList<NetworkState> result = Lists.newArrayList();
+        synchronized (mRulesLock) {
+            for (int networkType = 0; networkType <= ConnectivityManager.MAX_NETWORK_TYPE;
+                    networkType++) {
+                if (getNetworkInfoForType(networkType) != null) {
+                    final NetworkInfo info = getFilteredNetworkInfo(networkType, uid);
+                    final LinkProperties lp = getLinkPropertiesForTypeInternal(networkType);
+                    final NetworkCapabilities netcap = getNetworkCapabilitiesForType(networkType);
+                    result.add(new NetworkState(info, lp, netcap));
+                }
+            }
+        }
+        return result.toArray(new NetworkState[result.size()]);
+    }
+
+    private NetworkState getNetworkStateUnchecked(int networkType) {
+        if (isNetworkTypeValid(networkType)) {
+            NetworkInfo info = getNetworkInfoForType(networkType);
+            if (info != null) {
+                return new NetworkState(info,
+                        getLinkPropertiesForTypeInternal(networkType),
+                        getNetworkCapabilitiesForType(networkType));
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public NetworkQuotaInfo getActiveNetworkQuotaInfo() {
+        enforceAccessPermission();
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            final NetworkState state = getNetworkStateUnchecked(mActiveDefaultNetwork);
+            if (state != null) {
+                try {
+                    return mPolicyManager.getNetworkQuotaInfo(state);
+                } catch (RemoteException e) {
+                }
+            }
+            return null;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public boolean isActiveNetworkMetered() {
+        enforceAccessPermission();
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return isNetworkMeteredUnchecked(mActiveDefaultNetwork);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    private boolean isNetworkMeteredUnchecked(int networkType) {
+        final NetworkState state = getNetworkStateUnchecked(networkType);
+        if (state != null) {
+            try {
+                return mPolicyManager.isNetworkMetered(state);
+            } catch (RemoteException e) {
+            }
+        }
+        return false;
+    }
+
+    private INetworkManagementEventObserver mDataActivityObserver = new BaseNetworkObserver() {
+        @Override
+        public void interfaceClassDataActivityChanged(String label, boolean active, long tsNanos) {
+            int deviceType = Integer.parseInt(label);
+            sendDataActivityBroadcast(deviceType, active, tsNanos);
+        }
+    };
+
+    /**
+     * Used to notice when the calling process dies so we can self-expire
+     *
+     * Also used to know if the process has cleaned up after itself when
+     * our auto-expire timer goes off.  The timer has a link to an object.
+     *
+     */
+    private class FeatureUser implements IBinder.DeathRecipient {
+        int mNetworkType;
+        String mFeature;
+        IBinder mBinder;
+        int mPid;
+        int mUid;
+        long mCreateTime;
+
+        FeatureUser(int type, String feature, IBinder binder) {
+            super();
+            mNetworkType = type;
+            mFeature = feature;
+            mBinder = binder;
+            mPid = getCallingPid();
+            mUid = getCallingUid();
+            mCreateTime = System.currentTimeMillis();
+
+            try {
+                mBinder.linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                binderDied();
+            }
+        }
+
+        void unlinkDeathRecipient() {
+            mBinder.unlinkToDeath(this, 0);
+        }
+
+        public void binderDied() {
+            log("ConnectivityService FeatureUser binderDied(" +
+                    mNetworkType + ", " + mFeature + ", " + mBinder + "), created " +
+                    (System.currentTimeMillis() - mCreateTime) + " mSec ago");
+            stopUsingNetworkFeature(this, false);
+        }
+
+        public void expire() {
+            if (VDBG) {
+                log("ConnectivityService FeatureUser expire(" +
+                        mNetworkType + ", " + mFeature + ", " + mBinder +"), created " +
+                        (System.currentTimeMillis() - mCreateTime) + " mSec ago");
+            }
+            stopUsingNetworkFeature(this, false);
+        }
+
+        public boolean isSameUser(FeatureUser u) {
+            if (u == null) return false;
+
+            return isSameUser(u.mPid, u.mUid, u.mNetworkType, u.mFeature);
+        }
+
+        public boolean isSameUser(int pid, int uid, int networkType, String feature) {
+            if ((mPid == pid) && (mUid == uid) && (mNetworkType == networkType) &&
+                TextUtils.equals(mFeature, feature)) {
+                return true;
+            }
+            return false;
+        }
+
+        public String toString() {
+            return "FeatureUser("+mNetworkType+","+mFeature+","+mPid+","+mUid+"), created " +
+                    (System.currentTimeMillis() - mCreateTime) + " mSec ago";
+        }
+    }
+
+    // javadoc from interface
+    public int startUsingNetworkFeature(int networkType, String feature,
+            IBinder binder) {
+        long startTime = 0;
+        if (DBG) {
+            startTime = SystemClock.elapsedRealtime();
+        }
+        if (VDBG) {
+            log("startUsingNetworkFeature for net " + networkType + ": " + feature + ", uid="
+                    + Binder.getCallingUid());
+        }
+        enforceChangePermission();
+        try {
+            if (!ConnectivityManager.isNetworkTypeValid(networkType) ||
+                    mNetConfigs[networkType] == null) {
+                return PhoneConstants.APN_REQUEST_FAILED;
+            }
+
+            FeatureUser f = new FeatureUser(networkType, feature, binder);
+
+            // TODO - move this into individual networktrackers
+            int usedNetworkType = convertFeatureToNetworkType(networkType, feature);
+
+            if (mLockdownEnabled) {
+                // Since carrier APNs usually aren't available from VPN
+                // endpoint, mark them as unavailable.
+                return PhoneConstants.APN_TYPE_NOT_AVAILABLE;
+            }
+
+            if (mProtectedNetworks.contains(usedNetworkType)) {
+                enforceConnectivityInternalPermission();
+            }
+
+            // if UID is restricted, don't allow them to bring up metered APNs
+            final boolean networkMetered = isNetworkMeteredUnchecked(usedNetworkType);
+            final int uidRules;
+            synchronized (mRulesLock) {
+                uidRules = mUidRules.get(Binder.getCallingUid(), RULE_ALLOW_ALL);
+            }
+            if (networkMetered && (uidRules & RULE_REJECT_METERED) != 0) {
+                return PhoneConstants.APN_REQUEST_FAILED;
+            }
+
+            NetworkStateTracker network = mNetTrackers[usedNetworkType];
+            if (network != null) {
+                Integer currentPid = new Integer(getCallingPid());
+                if (usedNetworkType != networkType) {
+                    NetworkInfo ni = network.getNetworkInfo();
+
+                    if (ni.isAvailable() == false) {
+                        if (!TextUtils.equals(feature,Phone.FEATURE_ENABLE_DUN_ALWAYS)) {
+                            if (DBG) log("special network not available ni=" + ni.getTypeName());
+                            return PhoneConstants.APN_TYPE_NOT_AVAILABLE;
+                        } else {
+                            // else make the attempt anyway - probably giving REQUEST_STARTED below
+                            if (DBG) {
+                                log("special network not available, but try anyway ni=" +
+                                        ni.getTypeName());
+                            }
+                        }
+                    }
+
+                    int restoreTimer = getRestoreDefaultNetworkDelay(usedNetworkType);
+
+                    synchronized(this) {
+                        boolean addToList = true;
+                        if (restoreTimer < 0) {
+                            // In case there is no timer is specified for the feature,
+                            // make sure we don't add duplicate entry with the same request.
+                            for (FeatureUser u : mFeatureUsers) {
+                                if (u.isSameUser(f)) {
+                                    // Duplicate user is found. Do not add.
+                                    addToList = false;
+                                    break;
+                                }
+                            }
+                        }
+
+                        if (addToList) mFeatureUsers.add(f);
+                        if (!mNetRequestersPids[usedNetworkType].contains(currentPid)) {
+                            // this gets used for per-pid dns when connected
+                            mNetRequestersPids[usedNetworkType].add(currentPid);
+                        }
+                    }
+
+                    if (restoreTimer >= 0) {
+                        mHandler.sendMessageDelayed(mHandler.obtainMessage(
+                                EVENT_RESTORE_DEFAULT_NETWORK, f), restoreTimer);
+                    }
+
+                    if ((ni.isConnectedOrConnecting() == true) &&
+                            !network.isTeardownRequested()) {
+                        if (ni.isConnected() == true) {
+                            final long token = Binder.clearCallingIdentity();
+                            try {
+                                // add the pid-specific dns
+                                handleDnsConfigurationChange(usedNetworkType);
+                                if (VDBG) log("special network already active");
+                            } finally {
+                                Binder.restoreCallingIdentity(token);
+                            }
+                            return PhoneConstants.APN_ALREADY_ACTIVE;
+                        }
+                        if (VDBG) log("special network already connecting");
+                        return PhoneConstants.APN_REQUEST_STARTED;
+                    }
+
+                    // check if the radio in play can make another contact
+                    // assume if cannot for now
+
+                    if (DBG) {
+                        log("startUsingNetworkFeature reconnecting to " + networkType + ": " +
+                                feature);
+                    }
+                    if (network.reconnect()) {
+                        if (DBG) log("startUsingNetworkFeature X: return APN_REQUEST_STARTED");
+                        return PhoneConstants.APN_REQUEST_STARTED;
+                    } else {
+                        if (DBG) log("startUsingNetworkFeature X: return APN_REQUEST_FAILED");
+                        return PhoneConstants.APN_REQUEST_FAILED;
+                    }
+                } else {
+                    // need to remember this unsupported request so we respond appropriately on stop
+                    synchronized(this) {
+                        mFeatureUsers.add(f);
+                        if (!mNetRequestersPids[usedNetworkType].contains(currentPid)) {
+                            // this gets used for per-pid dns when connected
+                            mNetRequestersPids[usedNetworkType].add(currentPid);
+                        }
+                    }
+                    if (DBG) log("startUsingNetworkFeature X: return -1 unsupported feature.");
+                    return -1;
+                }
+            }
+            if (DBG) log("startUsingNetworkFeature X: return APN_TYPE_NOT_AVAILABLE");
+            return PhoneConstants.APN_TYPE_NOT_AVAILABLE;
+         } finally {
+            if (DBG) {
+                final long execTime = SystemClock.elapsedRealtime() - startTime;
+                if (execTime > 250) {
+                    loge("startUsingNetworkFeature took too long: " + execTime + "ms");
+                } else {
+                    if (VDBG) log("startUsingNetworkFeature took " + execTime + "ms");
+                }
+            }
+         }
+    }
+
+    // javadoc from interface
+    public int stopUsingNetworkFeature(int networkType, String feature) {
+        enforceChangePermission();
+
+        int pid = getCallingPid();
+        int uid = getCallingUid();
+
+        FeatureUser u = null;
+        boolean found = false;
+
+        synchronized(this) {
+            for (FeatureUser x : mFeatureUsers) {
+                if (x.isSameUser(pid, uid, networkType, feature)) {
+                    u = x;
+                    found = true;
+                    break;
+                }
+            }
+        }
+        if (found && u != null) {
+            if (VDBG) log("stopUsingNetworkFeature: X");
+            // stop regardless of how many other time this proc had called start
+            return stopUsingNetworkFeature(u, true);
+        } else {
+            // none found!
+            if (VDBG) log("stopUsingNetworkFeature: X not a live request, ignoring");
+            return 1;
+        }
+    }
+
+    private int stopUsingNetworkFeature(FeatureUser u, boolean ignoreDups) {
+        int networkType = u.mNetworkType;
+        String feature = u.mFeature;
+        int pid = u.mPid;
+        int uid = u.mUid;
+
+        NetworkStateTracker tracker = null;
+        boolean callTeardown = false;  // used to carry our decision outside of sync block
+
+        if (VDBG) {
+            log("stopUsingNetworkFeature: net " + networkType + ": " + feature);
+        }
+
+        if (!ConnectivityManager.isNetworkTypeValid(networkType)) {
+            if (DBG) {
+                log("stopUsingNetworkFeature: net " + networkType + ": " + feature +
+                        ", net is invalid");
+            }
+            return -1;
+        }
+
+        // need to link the mFeatureUsers list with the mNetRequestersPids state in this
+        // sync block
+        synchronized(this) {
+            // check if this process still has an outstanding start request
+            if (!mFeatureUsers.contains(u)) {
+                if (VDBG) {
+                    log("stopUsingNetworkFeature: this process has no outstanding requests" +
+                        ", ignoring");
+                }
+                return 1;
+            }
+            u.unlinkDeathRecipient();
+            mFeatureUsers.remove(mFeatureUsers.indexOf(u));
+            // If we care about duplicate requests, check for that here.
+            //
+            // This is done to support the extension of a request - the app
+            // can request we start the network feature again and renew the
+            // auto-shutoff delay.  Normal "stop" calls from the app though
+            // do not pay attention to duplicate requests - in effect the
+            // API does not refcount and a single stop will counter multiple starts.
+            if (ignoreDups == false) {
+                for (FeatureUser x : mFeatureUsers) {
+                    if (x.isSameUser(u)) {
+                        if (VDBG) log("stopUsingNetworkFeature: dup is found, ignoring");
+                        return 1;
+                    }
+                }
+            }
+
+            // TODO - move to individual network trackers
+            int usedNetworkType = convertFeatureToNetworkType(networkType, feature);
+
+            tracker =  mNetTrackers[usedNetworkType];
+            if (tracker == null) {
+                if (DBG) {
+                    log("stopUsingNetworkFeature: net " + networkType + ": " + feature +
+                            " no known tracker for used net type " + usedNetworkType);
+                }
+                return -1;
+            }
+            if (usedNetworkType != networkType) {
+                Integer currentPid = new Integer(pid);
+                mNetRequestersPids[usedNetworkType].remove(currentPid);
+
+                final long token = Binder.clearCallingIdentity();
+                try {
+                    reassessPidDns(pid, true);
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
+                flushVmDnsCache();
+                if (mNetRequestersPids[usedNetworkType].size() != 0) {
+                    if (VDBG) {
+                        log("stopUsingNetworkFeature: net " + networkType + ": " + feature +
+                                " others still using it");
+                    }
+                    return 1;
+                }
+                callTeardown = true;
+            } else {
+                if (DBG) {
+                    log("stopUsingNetworkFeature: net " + networkType + ": " + feature +
+                            " not a known feature - dropping");
+                }
+            }
+        }
+
+        if (callTeardown) {
+            if (DBG) {
+                log("stopUsingNetworkFeature: teardown net " + networkType + ": " + feature);
+            }
+            tracker.teardown();
+            return 1;
+        } else {
+            return -1;
+        }
+    }
+
+    /**
+     * Check if the address falls into any of currently running VPN's route's.
+     */
+    private boolean isAddressUnderVpn(InetAddress address) {
+        synchronized (mVpns) {
+            synchronized (mRoutesLock) {
+                int uid = UserHandle.getCallingUserId();
+                Vpn vpn = mVpns.get(uid);
+                if (vpn == null) {
+                    return false;
+                }
+
+                // Check if an exemption exists for this address.
+                for (LinkAddress destination : mExemptAddresses) {
+                    if (!NetworkUtils.addressTypeMatches(address, destination.getAddress())) {
+                        continue;
+                    }
+
+                    int prefix = destination.getPrefixLength();
+                    InetAddress addrMasked = NetworkUtils.getNetworkPart(address, prefix);
+                    InetAddress destMasked = NetworkUtils.getNetworkPart(destination.getAddress(),
+                            prefix);
+
+                    if (addrMasked.equals(destMasked)) {
+                        return false;
+                    }
+                }
+
+                // Finally check if the address is covered by the VPN.
+                return vpn.isAddressCovered(address);
+            }
+        }
+    }
+
+    /**
+     * @deprecated use requestRouteToHostAddress instead
+     *
+     * Ensure that a network route exists to deliver traffic to the specified
+     * host via the specified network interface.
+     * @param networkType the type of the network over which traffic to the
+     * specified host is to be routed
+     * @param hostAddress the IP address of the host to which the route is
+     * desired
+     * @return {@code true} on success, {@code false} on failure
+     */
+    public boolean requestRouteToHost(int networkType, int hostAddress, String packageName) {
+        InetAddress inetAddress = NetworkUtils.intToInetAddress(hostAddress);
+
+        if (inetAddress == null) {
+            return false;
+        }
+
+        return requestRouteToHostAddress(networkType, inetAddress.getAddress(), packageName);
+    }
+
+    /**
+     * Ensure that a network route exists to deliver traffic to the specified
+     * host via the specified network interface.
+     * @param networkType the type of the network over which traffic to the
+     * specified host is to be routed
+     * @param hostAddress the IP address of the host to which the route is
+     * desired
+     * @return {@code true} on success, {@code false} on failure
+     */
+    public boolean requestRouteToHostAddress(int networkType, byte[] hostAddress,
+            String packageName) {
+        enforceChangePermission();
+        if (mProtectedNetworks.contains(networkType)) {
+            enforceConnectivityInternalPermission();
+        }
+        boolean exempt;
+        InetAddress addr;
+        try {
+            addr = InetAddress.getByAddress(hostAddress);
+        } catch (UnknownHostException e) {
+            if (DBG) log("requestRouteToHostAddress got " + e.toString());
+            return false;
+        }
+        // System apps may request routes bypassing the VPN to keep other networks working.
+        if (Binder.getCallingUid() == Process.SYSTEM_UID) {
+            exempt = true;
+        } else {
+            mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName);
+            try {
+                ApplicationInfo info = mContext.getPackageManager().getApplicationInfo(packageName,
+                        0);
+                exempt = (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+            } catch (NameNotFoundException e) {
+                throw new IllegalArgumentException("Failed to find calling package details", e);
+            }
+        }
+
+        // Non-exempt routeToHost's can only be added if the host is not covered by the VPN.
+        // This can be either because the VPN's routes do not cover the destination or a
+        // system application added an exemption that covers this destination.
+        if (!exempt && isAddressUnderVpn(addr)) {
+            return false;
+        }
+
+        if (!ConnectivityManager.isNetworkTypeValid(networkType)) {
+            if (DBG) log("requestRouteToHostAddress on invalid network: " + networkType);
+            return false;
+        }
+
+        NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+        if (nai == null) {
+            if (mLegacyTypeTracker.isTypeSupported(networkType) == false) {
+                if (DBG) log("requestRouteToHostAddress on unsupported network: " + networkType);
+            } else {
+                if (DBG) log("requestRouteToHostAddress on down network: " + networkType);
+            }
+            return false;
+        }
+
+        DetailedState netState = nai.networkInfo.getDetailedState();
+
+        if ((netState != DetailedState.CONNECTED &&
+                netState != DetailedState.CAPTIVE_PORTAL_CHECK)) {
+            if (VDBG) {
+                log("requestRouteToHostAddress on down network "
+                        + "(" + networkType + ") - dropped"
+                        + " netState=" + netState);
+            }
+            return false;
+        }
+        final int uid = Binder.getCallingUid();
+        final long token = Binder.clearCallingIdentity();
+        try {
+            LinkProperties lp = nai.linkProperties;
+            boolean ok = modifyRouteToAddress(lp, addr, ADD, TO_DEFAULT_TABLE, exempt,
+                    nai.network.netId, uid);
+            if (DBG) log("requestRouteToHostAddress ok=" + ok);
+            return ok;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    private boolean addRoute(LinkProperties p, RouteInfo r, boolean toDefaultTable,
+            boolean exempt, int netId) {
+        return modifyRoute(p, r, 0, ADD, toDefaultTable, exempt, netId, false, UID_UNUSED);
+    }
+
+    private boolean removeRoute(LinkProperties p, RouteInfo r, boolean toDefaultTable, int netId) {
+        return modifyRoute(p, r, 0, REMOVE, toDefaultTable, UNEXEMPT, netId, false, UID_UNUSED);
+    }
+
+    private boolean modifyRouteToAddress(LinkProperties lp, InetAddress addr, boolean doAdd,
+            boolean toDefaultTable, boolean exempt, int netId, int uid) {
+        RouteInfo bestRoute = RouteInfo.selectBestRoute(lp.getAllRoutes(), addr);
+        if (bestRoute == null) {
+            bestRoute = RouteInfo.makeHostRoute(addr, lp.getInterfaceName());
+        } else {
+            String iface = bestRoute.getInterface();
+            if (bestRoute.getGateway().equals(addr)) {
+                // if there is no better route, add the implied hostroute for our gateway
+                bestRoute = RouteInfo.makeHostRoute(addr, iface);
+            } else {
+                // if we will connect to this through another route, add a direct route
+                // to it's gateway
+                bestRoute = RouteInfo.makeHostRoute(addr, bestRoute.getGateway(), iface);
+            }
+        }
+        return modifyRoute(lp, bestRoute, 0, doAdd, toDefaultTable, exempt, netId, true, uid);
+    }
+
+    /*
+     * TODO: Clean all this stuff up. Once we have UID-based routing, stuff will break due to
+     *       incorrect tracking of mAddedRoutes, so a cleanup becomes necessary and urgent. But at
+     *       the same time, there'll be no more need to track mAddedRoutes or mExemptAddresses,
+     *       or even have the concept of an exempt address, or do things like "selectBestRoute", or
+     *       determine "default" vs "secondary" table, etc., so the cleanup becomes possible.
+     */
+    private boolean modifyRoute(LinkProperties lp, RouteInfo r, int cycleCount, boolean doAdd,
+            boolean toDefaultTable, boolean exempt, int netId, boolean legacy, int uid) {
+        if ((lp == null) || (r == null)) {
+            if (DBG) log("modifyRoute got unexpected null: " + lp + ", " + r);
+            return false;
+        }
+
+        if (cycleCount > MAX_HOSTROUTE_CYCLE_COUNT) {
+            loge("Error modifying route - too much recursion");
+            return false;
+        }
+
+        String ifaceName = r.getInterface();
+        if(ifaceName == null) {
+            loge("Error modifying route - no interface name");
+            return false;
+        }
+        if (r.hasGateway()) {
+            RouteInfo bestRoute = RouteInfo.selectBestRoute(lp.getAllRoutes(), r.getGateway());
+            if (bestRoute != null) {
+                if (bestRoute.getGateway().equals(r.getGateway())) {
+                    // if there is no better route, add the implied hostroute for our gateway
+                    bestRoute = RouteInfo.makeHostRoute(r.getGateway(), ifaceName);
+                } else {
+                    // if we will connect to our gateway through another route, add a direct
+                    // route to it's gateway
+                    bestRoute = RouteInfo.makeHostRoute(r.getGateway(),
+                                                        bestRoute.getGateway(),
+                                                        ifaceName);
+                }
+                modifyRoute(lp, bestRoute, cycleCount+1, doAdd, toDefaultTable, exempt, netId,
+                        legacy, uid);
+            }
+        }
+        if (doAdd) {
+            if (VDBG) log("Adding " + r + " for interface " + ifaceName);
+            try {
+                if (toDefaultTable) {
+                    synchronized (mRoutesLock) {
+                        // only track default table - only one apps can effect
+                        mAddedRoutes.add(r);
+                        if (legacy) {
+                            mNetd.addLegacyRouteForNetId(netId, r, uid);
+                        } else {
+                            mNetd.addRoute(netId, r);
+                        }
+                        if (exempt) {
+                            LinkAddress dest = r.getDestinationLinkAddress();
+                            if (!mExemptAddresses.contains(dest)) {
+                                mNetd.setHostExemption(dest);
+                                mExemptAddresses.add(dest);
+                            }
+                        }
+                    }
+                } else {
+                    if (legacy) {
+                        mNetd.addLegacyRouteForNetId(netId, r, uid);
+                    } else {
+                        mNetd.addRoute(netId, r);
+                    }
+                }
+            } catch (Exception e) {
+                // never crash - catch them all
+                if (DBG) loge("Exception trying to add a route: " + e);
+                return false;
+            }
+        } else {
+            // if we remove this one and there are no more like it, then refcount==0 and
+            // we can remove it from the table
+            if (toDefaultTable) {
+                synchronized (mRoutesLock) {
+                    mAddedRoutes.remove(r);
+                    if (mAddedRoutes.contains(r) == false) {
+                        if (VDBG) log("Removing " + r + " for interface " + ifaceName);
+                        try {
+                            if (legacy) {
+                                mNetd.removeLegacyRouteForNetId(netId, r, uid);
+                            } else {
+                                mNetd.removeRoute(netId, r);
+                            }
+                            LinkAddress dest = r.getDestinationLinkAddress();
+                            if (mExemptAddresses.contains(dest)) {
+                                mNetd.clearHostExemption(dest);
+                                mExemptAddresses.remove(dest);
+                            }
+                        } catch (Exception e) {
+                            // never crash - catch them all
+                            if (VDBG) loge("Exception trying to remove a route: " + e);
+                            return false;
+                        }
+                    } else {
+                        if (VDBG) log("not removing " + r + " as it's still in use");
+                    }
+                }
+            } else {
+                if (VDBG) log("Removing " + r + " for interface " + ifaceName);
+                try {
+                    if (legacy) {
+                        mNetd.removeLegacyRouteForNetId(netId, r, uid);
+                    } else {
+                        mNetd.removeRoute(netId, r);
+                    }
+                } catch (Exception e) {
+                    // never crash - catch them all
+                    if (VDBG) loge("Exception trying to remove a route: " + e);
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    public void setDataDependency(int networkType, boolean met) {
+        enforceConnectivityInternalPermission();
+
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_DEPENDENCY_MET,
+                (met ? ENABLED : DISABLED), networkType));
+    }
+
+    private void handleSetDependencyMet(int networkType, boolean met) {
+        if (mNetTrackers[networkType] != null) {
+            if (DBG) {
+                log("handleSetDependencyMet(" + networkType + ", " + met + ")");
+            }
+            mNetTrackers[networkType].setDependencyMet(met);
+        }
+    }
+
+    private INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
+        @Override
+        public void onUidRulesChanged(int uid, int uidRules) {
+            // caller is NPMS, since we only register with them
+            if (LOGD_RULES) {
+                log("onUidRulesChanged(uid=" + uid + ", uidRules=" + uidRules + ")");
+            }
+
+            synchronized (mRulesLock) {
+                // skip update when we've already applied rules
+                final int oldRules = mUidRules.get(uid, RULE_ALLOW_ALL);
+                if (oldRules == uidRules) return;
+
+                mUidRules.put(uid, uidRules);
+            }
+
+            // TODO: notify UID when it has requested targeted updates
+        }
+
+        @Override
+        public void onMeteredIfacesChanged(String[] meteredIfaces) {
+            // caller is NPMS, since we only register with them
+            if (LOGD_RULES) {
+                log("onMeteredIfacesChanged(ifaces=" + Arrays.toString(meteredIfaces) + ")");
+            }
+
+            synchronized (mRulesLock) {
+                mMeteredIfaces.clear();
+                for (String iface : meteredIfaces) {
+                    mMeteredIfaces.add(iface);
+                }
+            }
+        }
+
+        @Override
+        public void onRestrictBackgroundChanged(boolean restrictBackground) {
+            // caller is NPMS, since we only register with them
+            if (LOGD_RULES) {
+                log("onRestrictBackgroundChanged(restrictBackground=" + restrictBackground + ")");
+            }
+
+            // kick off connectivity change broadcast for active network, since
+            // global background policy change is radical.
+            final int networkType = mActiveDefaultNetwork;
+            if (isNetworkTypeValid(networkType)) {
+                final NetworkStateTracker tracker = mNetTrackers[networkType];
+                if (tracker != null) {
+                    final NetworkInfo info = tracker.getNetworkInfo();
+                    if (info != null && info.isConnected()) {
+                        sendConnectedBroadcast(info);
+                    }
+                }
+            }
+        }
+    };
+
+    @Override
+    public void setPolicyDataEnable(int networkType, boolean enabled) {
+        // only someone like NPMS should only be calling us
+        mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
+
+        mHandler.sendMessage(mHandler.obtainMessage(
+                EVENT_SET_POLICY_DATA_ENABLE, networkType, (enabled ? ENABLED : DISABLED)));
+    }
+
+    private void handleSetPolicyDataEnable(int networkType, boolean enabled) {
+   // TODO - handle this passing to factories
+//        if (isNetworkTypeValid(networkType)) {
+//            final NetworkStateTracker tracker = mNetTrackers[networkType];
+//            if (tracker != null) {
+//                tracker.setPolicyDataEnable(enabled);
+//            }
+//        }
+    }
+
+    private void enforceAccessPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.ACCESS_NETWORK_STATE,
+                "ConnectivityService");
+    }
+
+    private void enforceChangePermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CHANGE_NETWORK_STATE,
+                "ConnectivityService");
+    }
+
+    // TODO Make this a special check when it goes public
+    private void enforceTetherChangePermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CHANGE_NETWORK_STATE,
+                "ConnectivityService");
+    }
+
+    private void enforceTetherAccessPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.ACCESS_NETWORK_STATE,
+                "ConnectivityService");
+    }
+
+    private void enforceConnectivityInternalPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CONNECTIVITY_INTERNAL,
+                "ConnectivityService");
+    }
+
+    private void enforceMarkNetworkSocketPermission() {
+        //Media server special case
+        if (Binder.getCallingUid() == Process.MEDIA_UID) {
+            return;
+        }
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MARK_NETWORK_SOCKET,
+                "ConnectivityService");
+    }
+
+    /**
+     * Handle a {@code DISCONNECTED} event. If this pertains to the non-active
+     * network, we ignore it. If it is for the active network, we send out a
+     * broadcast. But first, we check whether it might be possible to connect
+     * to a different network.
+     * @param info the {@code NetworkInfo} for the network
+     */
+    private void handleDisconnect(NetworkInfo info) {
+
+        int prevNetType = info.getType();
+
+        mNetTrackers[prevNetType].setTeardownRequested(false);
+        int thisNetId = mNetTrackers[prevNetType].getNetwork().netId;
+
+        // Remove idletimer previously setup in {@code handleConnect}
+// Already in place in new function. This is dead code.
+//        if (mNetConfigs[prevNetType].isDefault()) {
+//            removeDataActivityTracking(prevNetType);
+//        }
+
+        /*
+         * If the disconnected network is not the active one, then don't report
+         * this as a loss of connectivity. What probably happened is that we're
+         * getting the disconnect for a network that we explicitly disabled
+         * in accordance with network preference policies.
+         */
+        if (!mNetConfigs[prevNetType].isDefault()) {
+            List<Integer> pids = mNetRequestersPids[prevNetType];
+            for (Integer pid : pids) {
+                // will remove them because the net's no longer connected
+                // need to do this now as only now do we know the pids and
+                // can properly null things that are no longer referenced.
+                reassessPidDns(pid.intValue(), false);
+            }
+        }
+
+        Intent intent = new Intent(ConnectivityManager.CONNECTIVITY_ACTION);
+        intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, new NetworkInfo(info));
+        intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType());
+        if (info.isFailover()) {
+            intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true);
+            info.setFailover(false);
+        }
+        if (info.getReason() != null) {
+            intent.putExtra(ConnectivityManager.EXTRA_REASON, info.getReason());
+        }
+        if (info.getExtraInfo() != null) {
+            intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO,
+                    info.getExtraInfo());
+        }
+
+        if (mNetConfigs[prevNetType].isDefault()) {
+            tryFailover(prevNetType);
+            if (mActiveDefaultNetwork != -1) {
+                NetworkInfo switchTo = mNetTrackers[mActiveDefaultNetwork].getNetworkInfo();
+                intent.putExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO, switchTo);
+            } else {
+                mDefaultInetConditionPublished = 0; // we're not connected anymore
+                intent.putExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, true);
+            }
+        }
+        intent.putExtra(ConnectivityManager.EXTRA_INET_CONDITION, mDefaultInetConditionPublished);
+
+        // Reset interface if no other connections are using the same interface
+        boolean doReset = true;
+        LinkProperties linkProperties = mNetTrackers[prevNetType].getLinkProperties();
+        if (linkProperties != null) {
+            String oldIface = linkProperties.getInterfaceName();
+            if (TextUtils.isEmpty(oldIface) == false) {
+                for (NetworkStateTracker networkStateTracker : mNetTrackers) {
+                    if (networkStateTracker == null) continue;
+                    NetworkInfo networkInfo = networkStateTracker.getNetworkInfo();
+                    if (networkInfo.isConnected() && networkInfo.getType() != prevNetType) {
+                        LinkProperties l = networkStateTracker.getLinkProperties();
+                        if (l == null) continue;
+                        if (oldIface.equals(l.getInterfaceName())) {
+                            doReset = false;
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        // do this before we broadcast the change
+// Already done in new function. This is dead code.
+//        handleConnectivityChange(prevNetType, doReset);
+
+        final Intent immediateIntent = new Intent(intent);
+        immediateIntent.setAction(CONNECTIVITY_ACTION_IMMEDIATE);
+        sendStickyBroadcast(immediateIntent);
+        sendStickyBroadcastDelayed(intent, getConnectivityChangeDelay());
+        /*
+         * If the failover network is already connected, then immediately send
+         * out a followup broadcast indicating successful failover
+         */
+        if (mActiveDefaultNetwork != -1) {
+            sendConnectedBroadcastDelayed(mNetTrackers[mActiveDefaultNetwork].getNetworkInfo(),
+                    getConnectivityChangeDelay());
+        }
+        try {
+//            mNetd.removeNetwork(thisNetId);
+        } catch (Exception e) {
+            loge("Exception removing network: " + e);
+        } finally {
+            mNetTrackers[prevNetType].setNetId(INVALID_NET_ID);
+        }
+    }
+
+    private void tryFailover(int prevNetType) {
+        /*
+         * If this is a default network, check if other defaults are available.
+         * Try to reconnect on all available and let them hash it out when
+         * more than one connects.
+         */
+        if (mNetConfigs[prevNetType].isDefault()) {
+            if (mActiveDefaultNetwork == prevNetType) {
+                if (DBG) {
+                    log("tryFailover: set mActiveDefaultNetwork=-1, prevNetType=" + prevNetType);
+                }
+                mActiveDefaultNetwork = -1;
+                try {
+                    mNetd.clearDefaultNetId();
+                } catch (Exception e) {
+                    loge("Exception clearing default network :" + e);
+                }
+            }
+
+            // don't signal a reconnect for anything lower or equal priority than our
+            // current connected default
+            // TODO - don't filter by priority now - nice optimization but risky
+//            int currentPriority = -1;
+//            if (mActiveDefaultNetwork != -1) {
+//                currentPriority = mNetConfigs[mActiveDefaultNetwork].mPriority;
+//            }
+
+            for (int checkType=0; checkType <= ConnectivityManager.MAX_NETWORK_TYPE; checkType++) {
+                if (checkType == prevNetType) continue;
+                if (mNetConfigs[checkType] == null) continue;
+                if (!mNetConfigs[checkType].isDefault()) continue;
+                if (mNetTrackers[checkType] == null) continue;
+
+// Enabling the isAvailable() optimization caused mobile to not get
+// selected if it was in the middle of error handling. Specifically
+// a moble connection that took 30 seconds to complete the DEACTIVATE_DATA_CALL
+// would not be available and we wouldn't get connected to anything.
+// So removing the isAvailable() optimization below for now. TODO: This
+// optimization should work and we need to investigate why it doesn't work.
+// This could be related to how DEACTIVATE_DATA_CALL is reporting its
+// complete before it is really complete.
+
+//                if (!mNetTrackers[checkType].isAvailable()) continue;
+
+//                if (currentPriority >= mNetConfigs[checkType].mPriority) continue;
+
+                NetworkStateTracker checkTracker = mNetTrackers[checkType];
+                NetworkInfo checkInfo = checkTracker.getNetworkInfo();
+                if (!checkInfo.isConnectedOrConnecting() || checkTracker.isTeardownRequested()) {
+                    checkInfo.setFailover(true);
+                    checkTracker.reconnect();
+                }
+                if (DBG) log("Attempting to switch to " + checkInfo.getTypeName());
+            }
+        }
+    }
+
+    public void sendConnectedBroadcast(NetworkInfo info) {
+        enforceConnectivityInternalPermission();
+        sendGeneralBroadcast(info, CONNECTIVITY_ACTION_IMMEDIATE);
+        sendGeneralBroadcast(info, CONNECTIVITY_ACTION);
+    }
+
+    private void sendConnectedBroadcastDelayed(NetworkInfo info, int delayMs) {
+        sendGeneralBroadcast(info, CONNECTIVITY_ACTION_IMMEDIATE);
+        sendGeneralBroadcastDelayed(info, CONNECTIVITY_ACTION, delayMs);
+    }
+
+    private void sendInetConditionBroadcast(NetworkInfo info) {
+        sendGeneralBroadcast(info, ConnectivityManager.INET_CONDITION_ACTION);
+    }
+
+    private Intent makeGeneralIntent(NetworkInfo info, String bcastType) {
+        if (mLockdownTracker != null) {
+            info = mLockdownTracker.augmentNetworkInfo(info);
+        }
+
+        Intent intent = new Intent(bcastType);
+        intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, new NetworkInfo(info));
+        intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType());
+        if (info.isFailover()) {
+            intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true);
+            info.setFailover(false);
+        }
+        if (info.getReason() != null) {
+            intent.putExtra(ConnectivityManager.EXTRA_REASON, info.getReason());
+        }
+        if (info.getExtraInfo() != null) {
+            intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO,
+                    info.getExtraInfo());
+        }
+        intent.putExtra(ConnectivityManager.EXTRA_INET_CONDITION, mDefaultInetConditionPublished);
+        return intent;
+    }
+
+    private void sendGeneralBroadcast(NetworkInfo info, String bcastType) {
+        sendStickyBroadcast(makeGeneralIntent(info, bcastType));
+    }
+
+    private void sendGeneralBroadcastDelayed(NetworkInfo info, String bcastType, int delayMs) {
+        sendStickyBroadcastDelayed(makeGeneralIntent(info, bcastType), delayMs);
+    }
+
+    private void sendDataActivityBroadcast(int deviceType, boolean active, long tsNanos) {
+        Intent intent = new Intent(ConnectivityManager.ACTION_DATA_ACTIVITY_CHANGE);
+        intent.putExtra(ConnectivityManager.EXTRA_DEVICE_TYPE, deviceType);
+        intent.putExtra(ConnectivityManager.EXTRA_IS_ACTIVE, active);
+        intent.putExtra(ConnectivityManager.EXTRA_REALTIME_NS, tsNanos);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL,
+                    RECEIVE_DATA_ACTIVITY_CHANGE, null, null, 0, null, null);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private void sendStickyBroadcast(Intent intent) {
+        synchronized(this) {
+            if (!mSystemReady) {
+                mInitialBroadcast = new Intent(intent);
+            }
+            intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+            if (VDBG) {
+                log("sendStickyBroadcast: action=" + intent.getAction());
+            }
+
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+    }
+
+    private void sendStickyBroadcastDelayed(Intent intent, int delayMs) {
+        if (delayMs <= 0) {
+            sendStickyBroadcast(intent);
+        } else {
+            if (VDBG) {
+                log("sendStickyBroadcastDelayed: delayMs=" + delayMs + ", action="
+                        + intent.getAction());
+            }
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(
+                    EVENT_SEND_STICKY_BROADCAST_INTENT, intent), delayMs);
+        }
+    }
+
+    void systemReady() {
+        mCaptivePortalTracker = CaptivePortalTracker.makeCaptivePortalTracker(mContext, this);
+        loadGlobalProxy();
+
+        synchronized(this) {
+            mSystemReady = true;
+            if (mInitialBroadcast != null) {
+                mContext.sendStickyBroadcastAsUser(mInitialBroadcast, UserHandle.ALL);
+                mInitialBroadcast = null;
+            }
+        }
+        // load the global proxy at startup
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_APPLY_GLOBAL_HTTP_PROXY));
+
+        // Try bringing up tracker, but if KeyStore isn't ready yet, wait
+        // for user to unlock device.
+        if (!updateLockdownVpn()) {
+            final IntentFilter filter = new IntentFilter(Intent.ACTION_USER_PRESENT);
+            mContext.registerReceiver(mUserPresentReceiver, filter);
+        }
+    }
+
+    private BroadcastReceiver mUserPresentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // Try creating lockdown tracker, since user present usually means
+            // unlocked keystore.
+            if (updateLockdownVpn()) {
+                mContext.unregisterReceiver(this);
+            }
+        }
+    };
+
+    private boolean isNewNetTypePreferredOverCurrentNetType(int type) {
+        if (((type != mNetworkPreference)
+                      && (mNetConfigs[mActiveDefaultNetwork].priority > mNetConfigs[type].priority))
+                   || (mNetworkPreference == mActiveDefaultNetwork)) {
+            return false;
+        }
+        return true;
+    }
+
+    private void handleConnect(NetworkInfo info) {
+        final int newNetType = info.getType();
+
+        // snapshot isFailover, because sendConnectedBroadcast() resets it
+        boolean isFailover = info.isFailover();
+        final NetworkStateTracker thisNet = mNetTrackers[newNetType];
+        final String thisIface = thisNet.getLinkProperties().getInterfaceName();
+
+        if (VDBG) {
+            log("handleConnect: E newNetType=" + newNetType + " thisIface=" + thisIface
+                    + " isFailover" + isFailover);
+        }
+
+        // if this is a default net and other default is running
+        // kill the one not preferred
+        if (mNetConfigs[newNetType].isDefault()) {
+            if (mActiveDefaultNetwork != -1 && mActiveDefaultNetwork != newNetType) {
+                if (isNewNetTypePreferredOverCurrentNetType(newNetType)) {
+                   String teardownPolicy = SystemProperties.get("net.teardownPolicy");
+                   if (TextUtils.equals(teardownPolicy, "keep") == false) {
+                        // tear down the other
+                        NetworkStateTracker otherNet =
+                                mNetTrackers[mActiveDefaultNetwork];
+                        if (DBG) {
+                            log("Policy requires " + otherNet.getNetworkInfo().getTypeName() +
+                                " teardown");
+                        }
+                        if (!teardown(otherNet)) {
+                            loge("Network declined teardown request");
+                            teardown(thisNet);
+                            return;
+                        }
+                    } else {
+                        //TODO - remove
+                        loge("network teardown skipped due to net.teardownPolicy setting");
+                    }
+                } else {
+                       // don't accept this one
+                        if (VDBG) {
+                            log("Not broadcasting CONNECT_ACTION " +
+                                "to torn down network " + info.getTypeName());
+                        }
+                        teardown(thisNet);
+                        return;
+                }
+            }
+            int thisNetId = nextNetId();
+            thisNet.setNetId(thisNetId);
+            try {
+//                mNetd.createNetwork(thisNetId, thisIface);
+            } catch (Exception e) {
+                loge("Exception creating network :" + e);
+                teardown(thisNet);
+                return;
+            }
+// Already in place in new function. This is dead code.
+//            setupDataActivityTracking(newNetType);
+            synchronized (ConnectivityService.this) {
+                // have a new default network, release the transition wakelock in a second
+                // if it's held.  The second pause is to allow apps to reconnect over the
+                // new network
+                if (mNetTransitionWakeLock.isHeld()) {
+                    mHandler.sendMessageDelayed(mHandler.obtainMessage(
+                            EVENT_CLEAR_NET_TRANSITION_WAKELOCK,
+                            mNetTransitionWakeLockSerialNumber, 0),
+                            1000);
+                }
+            }
+            mActiveDefaultNetwork = newNetType;
+            try {
+                mNetd.setDefaultNetId(thisNetId);
+            } catch (Exception e) {
+                loge("Exception setting default network :" + e);
+            }
+            // this will cause us to come up initially as unconnected and switching
+            // to connected after our normal pause unless somebody reports us as reall
+            // disconnected
+            mDefaultInetConditionPublished = 0;
+            mDefaultConnectionSequence++;
+            mInetConditionChangeInFlight = false;
+            // Don't do this - if we never sign in stay, grey
+            //reportNetworkCondition(mActiveDefaultNetwork, 100);
+            updateNetworkSettings(thisNet);
+        } else {
+            int thisNetId = nextNetId();
+            thisNet.setNetId(thisNetId);
+            try {
+//                mNetd.createNetwork(thisNetId, thisIface);
+            } catch (Exception e) {
+                loge("Exception creating network :" + e);
+                teardown(thisNet);
+                return;
+            }
+        }
+        thisNet.setTeardownRequested(false);
+// Already in place in new function. This is dead code.
+//        updateMtuSizeSettings(thisNet);
+//        handleConnectivityChange(newNetType, false);
+        sendConnectedBroadcastDelayed(info, getConnectivityChangeDelay());
+
+        // notify battery stats service about this network
+        if (thisIface != null) {
+            try {
+                BatteryStatsService.getService().noteNetworkInterfaceType(thisIface, newNetType);
+            } catch (RemoteException e) {
+                // ignored; service lives in system_server
+            }
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void captivePortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) {
+        enforceConnectivityInternalPermission();
+        if (DBG) log("captivePortalCheckCompleted: ni=" + info + " captive=" + isCaptivePortal);
+//        mNetTrackers[info.getType()].captivePortalCheckCompleted(isCaptivePortal);
+    }
+
+    /**
+     * Setup data activity tracking for the given network.
+     *
+     * Every {@code setupDataActivityTracking} should be paired with a
+     * {@link #removeDataActivityTracking} for cleanup.
+     */
+    private void setupDataActivityTracking(NetworkAgentInfo networkAgent) {
+        final String iface = networkAgent.linkProperties.getInterfaceName();
+
+        final int timeout;
+        int type = ConnectivityManager.TYPE_NONE;
+
+        if (networkAgent.networkCapabilities.hasTransport(
+                NetworkCapabilities.TRANSPORT_CELLULAR)) {
+            timeout = Settings.Global.getInt(mContext.getContentResolver(),
+                                             Settings.Global.DATA_ACTIVITY_TIMEOUT_MOBILE,
+                                             5);
+            type = ConnectivityManager.TYPE_MOBILE;
+        } else if (networkAgent.networkCapabilities.hasTransport(
+                NetworkCapabilities.TRANSPORT_WIFI)) {
+            timeout = Settings.Global.getInt(mContext.getContentResolver(),
+                                             Settings.Global.DATA_ACTIVITY_TIMEOUT_WIFI,
+                                             0);
+            type = ConnectivityManager.TYPE_WIFI;
+        } else {
+            // do not track any other networks
+            timeout = 0;
+        }
+
+        if (timeout > 0 && iface != null && type != ConnectivityManager.TYPE_NONE) {
+            try {
+                mNetd.addIdleTimer(iface, timeout, type);
+            } catch (Exception e) {
+                // You shall not crash!
+                loge("Exception in setupDataActivityTracking " + e);
+            }
+        }
+    }
+
+    /**
+     * Remove data activity tracking when network disconnects.
+     */
+    private void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+        final String iface = networkAgent.linkProperties.getInterfaceName();
+        final NetworkCapabilities caps = networkAgent.networkCapabilities;
+
+        if (iface != null && (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
+                              caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))) {
+            try {
+                // the call fails silently if no idletimer setup for this interface
+                mNetd.removeIdleTimer(iface);
+            } catch (Exception e) {
+                loge("Exception in removeDataActivityTracking " + e);
+            }
+        }
+    }
+
+    /**
+     * After a change in the connectivity state of a network. We're mainly
+     * concerned with making sure that the list of DNS servers is set up
+     * according to which networks are connected, and ensuring that the
+     * right routing table entries exist.
+     *
+     * TODO - delete when we're sure all this functionallity is captured.
+     */
+    private void handleConnectivityChange(int netType, LinkProperties curLp, boolean doReset) {
+        int resetMask = doReset ? NetworkUtils.RESET_ALL_ADDRESSES : 0;
+        boolean exempt = ConnectivityManager.isNetworkTypeExempt(netType);
+        if (VDBG) {
+            log("handleConnectivityChange: netType=" + netType + " doReset=" + doReset
+                    + " resetMask=" + resetMask);
+        }
+
+        /*
+         * If a non-default network is enabled, add the host routes that
+         * will allow it's DNS servers to be accessed.
+         */
+        handleDnsConfigurationChange(netType);
+
+        LinkProperties newLp = null;
+
+        if (mNetTrackers[netType].getNetworkInfo().isConnected()) {
+            newLp = mNetTrackers[netType].getLinkProperties();
+            if (VDBG) {
+                log("handleConnectivityChange: changed linkProperty[" + netType + "]:" +
+                        " doReset=" + doReset + " resetMask=" + resetMask +
+                        "\n   curLp=" + curLp +
+                        "\n   newLp=" + newLp);
+            }
+
+            if (curLp != null) {
+                if (curLp.isIdenticalInterfaceName(newLp)) {
+                    CompareResult<LinkAddress> car = curLp.compareAddresses(newLp);
+                    if ((car.removed.size() != 0) || (car.added.size() != 0)) {
+                        for (LinkAddress linkAddr : car.removed) {
+                            if (linkAddr.getAddress() instanceof Inet4Address) {
+                                resetMask |= NetworkUtils.RESET_IPV4_ADDRESSES;
+                            }
+                            if (linkAddr.getAddress() instanceof Inet6Address) {
+                                resetMask |= NetworkUtils.RESET_IPV6_ADDRESSES;
+                            }
+                        }
+                        if (DBG) {
+                            log("handleConnectivityChange: addresses changed" +
+                                    " linkProperty[" + netType + "]:" + " resetMask=" + resetMask +
+                                    "\n   car=" + car);
+                        }
+                    } else {
+                        if (VDBG) {
+                            log("handleConnectivityChange: addresses are the same reset per" +
+                                   " doReset linkProperty[" + netType + "]:" +
+                                   " resetMask=" + resetMask);
+                        }
+                    }
+                } else {
+                    resetMask = NetworkUtils.RESET_ALL_ADDRESSES;
+                    if (DBG) {
+                        log("handleConnectivityChange: interface not not equivalent reset both" +
+                                " linkProperty[" + netType + "]:" +
+                                " resetMask=" + resetMask);
+                    }
+                }
+            }
+            if (mNetConfigs[netType].isDefault()) {
+                handleApplyDefaultProxy(newLp.getHttpProxy());
+            }
+        } else {
+            if (VDBG) {
+                log("handleConnectivityChange: changed linkProperty[" + netType + "]:" +
+                        " doReset=" + doReset + " resetMask=" + resetMask +
+                        "\n  curLp=" + curLp +
+                        "\n  newLp= null");
+            }
+        }
+        mCurrentLinkProperties[netType] = newLp;
+        boolean resetDns = updateRoutes(newLp, curLp, mNetConfigs[netType].isDefault(), exempt,
+                                        mNetTrackers[netType].getNetwork().netId);
+
+        if (resetMask != 0 || resetDns) {
+            if (VDBG) log("handleConnectivityChange: resetting");
+            if (curLp != null) {
+                if (VDBG) log("handleConnectivityChange: resetting curLp=" + curLp);
+                for (String iface : curLp.getAllInterfaceNames()) {
+                    if (TextUtils.isEmpty(iface) == false) {
+                        if (resetMask != 0) {
+                            if (DBG) log("resetConnections(" + iface + ", " + resetMask + ")");
+                            NetworkUtils.resetConnections(iface, resetMask);
+
+                            // Tell VPN the interface is down. It is a temporary
+                            // but effective fix to make VPN aware of the change.
+                            if ((resetMask & NetworkUtils.RESET_IPV4_ADDRESSES) != 0) {
+                                synchronized(mVpns) {
+                                    for (int i = 0; i < mVpns.size(); i++) {
+                                        mVpns.valueAt(i).interfaceStatusChanged(iface, false);
+                                    }
+                                }
+                            }
+                        }
+                    } else {
+                        loge("Can't reset connection for type "+netType);
+                    }
+                }
+                if (resetDns) {
+                    flushVmDnsCache();
+                    if (VDBG) log("resetting DNS cache for type " + netType);
+                    try {
+                        mNetd.flushNetworkDnsCache(mNetTrackers[netType].getNetwork().netId);
+                    } catch (Exception e) {
+                        // never crash - catch them all
+                        if (DBG) loge("Exception resetting dns cache: " + e);
+                    }
+                }
+            }
+        }
+
+        // TODO: Temporary notifying upstread change to Tethering.
+        //       @see bug/4455071
+        /** Notify TetheringService if interface name has been changed. */
+        if (TextUtils.equals(mNetTrackers[netType].getNetworkInfo().getReason(),
+                             PhoneConstants.REASON_LINK_PROPERTIES_CHANGED)) {
+            if (isTetheringSupported()) {
+                mTethering.handleTetherIfaceChange();
+            }
+        }
+    }
+
+    /**
+     * Add and remove routes using the old properties (null if not previously connected),
+     * new properties (null if becoming disconnected).  May even be double null, which
+     * is a noop.
+     * Uses isLinkDefault to determine if default routes should be set or conversely if
+     * host routes should be set to the dns servers
+     * returns a boolean indicating the routes changed
+     */
+    private boolean updateRoutes(LinkProperties newLp, LinkProperties curLp,
+            boolean isLinkDefault, boolean exempt, int netId) {
+        Collection<RouteInfo> routesToAdd = null;
+        CompareResult<InetAddress> dnsDiff = new CompareResult<InetAddress>();
+        CompareResult<RouteInfo> routeDiff = new CompareResult<RouteInfo>();
+        if (curLp != null) {
+            // check for the delta between the current set and the new
+            routeDiff = curLp.compareAllRoutes(newLp);
+            dnsDiff = curLp.compareDnses(newLp);
+        } else if (newLp != null) {
+            routeDiff.added = newLp.getAllRoutes();
+            dnsDiff.added = newLp.getDnsServers();
+        }
+
+        boolean routesChanged = (routeDiff.removed.size() != 0 || routeDiff.added.size() != 0);
+
+        for (RouteInfo r : routeDiff.removed) {
+            if (isLinkDefault || ! r.isDefaultRoute()) {
+                if (VDBG) log("updateRoutes: default remove route r=" + r);
+                removeRoute(curLp, r, TO_DEFAULT_TABLE, netId);
+            }
+            if (isLinkDefault == false) {
+                // remove from a secondary route table
+                removeRoute(curLp, r, TO_SECONDARY_TABLE, netId);
+            }
+        }
+
+        for (RouteInfo r :  routeDiff.added) {
+            if (isLinkDefault || ! r.isDefaultRoute()) {
+                addRoute(newLp, r, TO_DEFAULT_TABLE, exempt, netId);
+            } else {
+                // add to a secondary route table
+                addRoute(newLp, r, TO_SECONDARY_TABLE, UNEXEMPT, netId);
+
+                // many radios add a default route even when we don't want one.
+                // remove the default route unless somebody else has asked for it
+                String ifaceName = newLp.getInterfaceName();
+                synchronized (mRoutesLock) {
+                    if (!TextUtils.isEmpty(ifaceName) && !mAddedRoutes.contains(r)) {
+                        if (VDBG) log("Removing " + r + " for interface " + ifaceName);
+                        try {
+                            mNetd.removeRoute(netId, r);
+                        } catch (Exception e) {
+                            // never crash - catch them all
+                            if (DBG) loge("Exception trying to remove a route: " + e);
+                        }
+                    }
+                }
+            }
+        }
+
+        return routesChanged;
+    }
+
+    /**
+     * Reads the network specific MTU size from reources.
+     * and set it on it's iface.
+     */
+    private void updateMtu(LinkProperties newLp, LinkProperties oldLp) {
+        final String iface = newLp.getInterfaceName();
+        final int mtu = newLp.getMtu();
+        if (oldLp != null && newLp.isIdenticalMtu(oldLp)) {
+            if (VDBG) log("identical MTU - not setting");
+            return;
+        }
+
+        if (mtu < 68 || mtu > 10000) {
+            loge("Unexpected mtu value: " + mtu + ", " + iface);
+            return;
+        }
+
+        try {
+            if (VDBG) log("Setting MTU size: " + iface + ", " + mtu);
+            mNetd.setMtu(iface, mtu);
+        } catch (Exception e) {
+            Slog.e(TAG, "exception in setMtu()" + e);
+        }
+    }
+
+    /**
+     * Reads the network specific TCP buffer sizes from SystemProperties
+     * net.tcp.buffersize.[default|wifi|umts|edge|gprs] and set them for system
+     * wide use
+     */
+    private void updateNetworkSettings(NetworkStateTracker nt) {
+        String key = nt.getTcpBufferSizesPropName();
+        String bufferSizes = key == null ? null : SystemProperties.get(key);
+
+        if (TextUtils.isEmpty(bufferSizes)) {
+            if (VDBG) log(key + " not found in system properties. Using defaults");
+
+            // Setting to default values so we won't be stuck to previous values
+            key = "net.tcp.buffersize.default";
+            bufferSizes = SystemProperties.get(key);
+        }
+
+        // Set values in kernel
+        if (bufferSizes.length() != 0) {
+            if (VDBG) {
+                log("Setting TCP values: [" + bufferSizes
+                        + "] which comes from [" + key + "]");
+            }
+            setBufferSize(bufferSizes);
+        }
+
+        final String defaultRwndKey = "net.tcp.default_init_rwnd";
+        int defaultRwndValue = SystemProperties.getInt(defaultRwndKey, 0);
+        Integer rwndValue = Settings.Global.getInt(mContext.getContentResolver(),
+            Settings.Global.TCP_DEFAULT_INIT_RWND, defaultRwndValue);
+        final String sysctlKey = "sys.sysctl.tcp_def_init_rwnd";
+        if (rwndValue != 0) {
+            SystemProperties.set(sysctlKey, rwndValue.toString());
+        }
+    }
+
+    /**
+     * Writes TCP buffer sizes to /sys/kernel/ipv4/tcp_[r/w]mem_[min/def/max]
+     * which maps to /proc/sys/net/ipv4/tcp_rmem and tcpwmem
+     *
+     * @param bufferSizes in the format of "readMin, readInitial, readMax,
+     *        writeMin, writeInitial, writeMax"
+     */
+    private void setBufferSize(String bufferSizes) {
+        try {
+            String[] values = bufferSizes.split(",");
+
+            if (values.length == 6) {
+              final String prefix = "/sys/kernel/ipv4/tcp_";
+                FileUtils.stringToFile(prefix + "rmem_min", values[0]);
+                FileUtils.stringToFile(prefix + "rmem_def", values[1]);
+                FileUtils.stringToFile(prefix + "rmem_max", values[2]);
+                FileUtils.stringToFile(prefix + "wmem_min", values[3]);
+                FileUtils.stringToFile(prefix + "wmem_def", values[4]);
+                FileUtils.stringToFile(prefix + "wmem_max", values[5]);
+            } else {
+                loge("Invalid buffersize string: " + bufferSizes);
+            }
+        } catch (IOException e) {
+            loge("Can't set tcp buffer sizes:" + e);
+        }
+    }
+
+    /**
+     * Adjust the per-process dns entries (net.dns<x>.<pid>) based
+     * on the highest priority active net which this process requested.
+     * If there aren't any, clear it out
+     */
+    private void reassessPidDns(int pid, boolean doBump)
+    {
+        if (VDBG) log("reassessPidDns for pid " + pid);
+        Integer myPid = new Integer(pid);
+        for(int i : mPriorityList) {
+            if (mNetConfigs[i].isDefault()) {
+                continue;
+            }
+            NetworkStateTracker nt = mNetTrackers[i];
+            if (nt.getNetworkInfo().isConnected() &&
+                    !nt.isTeardownRequested()) {
+                LinkProperties p = nt.getLinkProperties();
+                if (p == null) continue;
+                if (mNetRequestersPids[i].contains(myPid)) {
+                    try {
+                        // TODO: Reimplement this via local variable in bionic.
+                        // mNetd.setDnsNetworkForPid(nt.getNetwork().netId, pid);
+                    } catch (Exception e) {
+                        Slog.e(TAG, "exception reasseses pid dns: " + e);
+                    }
+                    return;
+                }
+           }
+        }
+        // nothing found - delete
+        try {
+            // TODO: Reimplement this via local variable in bionic.
+            // mNetd.clearDnsNetworkForPid(pid);
+        } catch (Exception e) {
+            Slog.e(TAG, "exception clear interface from pid: " + e);
+        }
+    }
+
+    private void flushVmDnsCache() {
+        /*
+         * Tell the VMs to toss their DNS caches
+         */
+        Intent intent = new Intent(Intent.ACTION_CLEAR_DNS_CACHE);
+        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+        /*
+         * Connectivity events can happen before boot has completed ...
+         */
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    // Caller must grab mDnsLock.
+    private void updateDnsLocked(String network, int netId,
+            Collection<InetAddress> dnses, String domains) {
+        int last = 0;
+        if (dnses.size() == 0 && mDefaultDns != null) {
+            dnses = new ArrayList();
+            dnses.add(mDefaultDns);
+            if (DBG) {
+                loge("no dns provided for " + network + " - using " + mDefaultDns.getHostAddress());
+            }
+        }
+
+        try {
+            mNetd.setDnsServersForNetwork(netId, NetworkUtils.makeStrings(dnses), domains);
+
+            for (InetAddress dns : dnses) {
+                ++last;
+                String key = "net.dns" + last;
+                String value = dns.getHostAddress();
+                SystemProperties.set(key, value);
+            }
+            for (int i = last + 1; i <= mNumDnsEntries; ++i) {
+                String key = "net.dns" + i;
+                SystemProperties.set(key, "");
+            }
+            mNumDnsEntries = last;
+        } catch (Exception e) {
+            loge("exception setting default dns interface: " + e);
+        }
+    }
+
+    private void handleDnsConfigurationChange(int netType) {
+        // add default net's dns entries
+        NetworkStateTracker nt = mNetTrackers[netType];
+        if (nt != null && nt.getNetworkInfo().isConnected() && !nt.isTeardownRequested()) {
+            LinkProperties p = nt.getLinkProperties();
+            if (p == null) return;
+            Collection<InetAddress> dnses = p.getDnsServers();
+            int netId = nt.getNetwork().netId;
+            if (mNetConfigs[netType].isDefault()) {
+                String network = nt.getNetworkInfo().getTypeName();
+                synchronized (mDnsLock) {
+                    updateDnsLocked(network, netId, dnses, p.getDomains());
+                }
+            } else {
+                try {
+                    mNetd.setDnsServersForNetwork(netId,
+                            NetworkUtils.makeStrings(dnses), p.getDomains());
+                } catch (Exception e) {
+                    if (DBG) loge("exception setting dns servers: " + e);
+                }
+                // set per-pid dns for attached secondary nets
+                List<Integer> pids = mNetRequestersPids[netType];
+                for (Integer pid : pids) {
+                    try {
+                        // TODO: Reimplement this via local variable in bionic.
+                        // mNetd.setDnsNetworkForPid(netId, pid);
+                    } catch (Exception e) {
+                        Slog.e(TAG, "exception setting interface for pid: " + e);
+                    }
+                }
+            }
+            flushVmDnsCache();
+        }
+    }
+
+    @Override
+    public int getRestoreDefaultNetworkDelay(int networkType) {
+        String restoreDefaultNetworkDelayStr = SystemProperties.get(
+                NETWORK_RESTORE_DELAY_PROP_NAME);
+        if(restoreDefaultNetworkDelayStr != null &&
+                restoreDefaultNetworkDelayStr.length() != 0) {
+            try {
+                return Integer.valueOf(restoreDefaultNetworkDelayStr);
+            } catch (NumberFormatException e) {
+            }
+        }
+        // if the system property isn't set, use the value for the apn type
+        int ret = RESTORE_DEFAULT_NETWORK_DELAY;
+
+        if ((networkType <= ConnectivityManager.MAX_NETWORK_TYPE) &&
+                (mNetConfigs[networkType] != null)) {
+            ret = mNetConfigs[networkType].restoreTime;
+        }
+        return ret;
+    }
+
+    @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 ConnectivityService " +
+                    "from from pid=" + Binder.getCallingPid() + ", uid=" +
+                    Binder.getCallingUid());
+            return;
+        }
+
+        pw.println("NetworkFactories for:");
+        pw.increaseIndent();
+        for (NetworkFactoryInfo nfi : mNetworkFactoryInfos.values()) {
+            pw.println(nfi.name);
+        }
+        pw.decreaseIndent();
+        pw.println();
+
+        NetworkAgentInfo defaultNai = mNetworkForRequestId.get(mDefaultRequest.requestId);
+        pw.print("Active default network: ");
+        if (defaultNai == null) {
+            pw.println("none");
+        } else {
+            pw.println(defaultNai.network.netId);
+        }
+        pw.println();
+
+        pw.println("Current Networks:");
+        pw.increaseIndent();
+        for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
+            pw.println(nai.toString());
+            pw.increaseIndent();
+            pw.println("Requests:");
+            pw.increaseIndent();
+            for (int i = 0; i < nai.networkRequests.size(); i++) {
+                pw.println(nai.networkRequests.valueAt(i).toString());
+            }
+            pw.decreaseIndent();
+            pw.println("Lingered:");
+            pw.increaseIndent();
+            for (NetworkRequest nr : nai.networkLingered) pw.println(nr.toString());
+            pw.decreaseIndent();
+            pw.decreaseIndent();
+        }
+        pw.decreaseIndent();
+        pw.println();
+
+        pw.println("Network Requests:");
+        pw.increaseIndent();
+        for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+            pw.println(nri.toString());
+        }
+        pw.println();
+        pw.decreaseIndent();
+
+        synchronized (this) {
+            pw.println("NetworkTranstionWakeLock is currently " +
+                    (mNetTransitionWakeLock.isHeld() ? "" : "not ") + "held.");
+            pw.println("It was last requested for "+mNetTransitionWakeLockCausedBy);
+        }
+        pw.println();
+
+        mTethering.dump(fd, pw, args);
+
+        if (mInetLog != null) {
+            pw.println();
+            pw.println("Inet condition reports:");
+            pw.increaseIndent();
+            for(int i = 0; i < mInetLog.size(); i++) {
+                pw.println(mInetLog.get(i));
+            }
+            pw.decreaseIndent();
+        }
+    }
+
+    // must be stateless - things change under us.
+    private class NetworkStateTrackerHandler extends Handler {
+        public NetworkStateTrackerHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            NetworkInfo info;
+            switch (msg.what) {
+                case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: {
+                    handleAsyncChannelHalfConnect(msg);
+                    break;
+                }
+                case AsyncChannel.CMD_CHANNEL_DISCONNECT: {
+                    NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);
+                    if (nai != null) nai.asyncChannel.disconnect();
+                    break;
+                }
+                case AsyncChannel.CMD_CHANNEL_DISCONNECTED: {
+                    handleAsyncChannelDisconnected(msg);
+                    break;
+                }
+                case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: {
+                    NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);
+                    if (nai == null) {
+                        loge("EVENT_NETWORK_CAPABILITIES_CHANGED from unknown NetworkAgent");
+                    } else {
+                        updateCapabilities(nai, (NetworkCapabilities)msg.obj);
+                    }
+                    break;
+                }
+                case NetworkAgent.EVENT_NETWORK_PROPERTIES_CHANGED: {
+                    NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);
+                    if (nai == null) {
+                        loge("NetworkAgent not found for EVENT_NETWORK_PROPERTIES_CHANGED");
+                    } else {
+                        if (VDBG) log("Update of Linkproperties for " + nai.name());
+                        LinkProperties oldLp = nai.linkProperties;
+                        nai.linkProperties = (LinkProperties)msg.obj;
+                        updateLinkProperties(nai, oldLp);
+                    }
+                    break;
+                }
+                case NetworkAgent.EVENT_NETWORK_INFO_CHANGED: {
+                    NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);
+                    if (nai == null) {
+                        loge("EVENT_NETWORK_INFO_CHANGED from unknown NetworkAgent");
+                        break;
+                    }
+                    info = (NetworkInfo) msg.obj;
+                    updateNetworkInfo(nai, info);
+                    break;
+                }
+                case NetworkAgent.EVENT_NETWORK_SCORE_CHANGED: {
+                    NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);
+                    if (nai == null) {
+                        loge("EVENT_NETWORK_SCORE_CHANGED from unknown NetworkAgent");
+                        break;
+                    }
+                    Integer score = (Integer) msg.obj;
+                    if (score != null) updateNetworkScore(nai, score.intValue());
+                    break;
+                }
+                case NetworkMonitor.EVENT_NETWORK_VALIDATED: {
+                    NetworkAgentInfo nai = (NetworkAgentInfo)msg.obj;
+                    handleConnectionValidated(nai);
+                    break;
+                }
+                case NetworkMonitor.EVENT_NETWORK_LINGER_COMPLETE: {
+                    NetworkAgentInfo nai = (NetworkAgentInfo)msg.obj;
+                    handleLingerComplete(nai);
+                    break;
+                }
+                case NetworkStateTracker.EVENT_STATE_CHANGED: {
+                    info = (NetworkInfo) msg.obj;
+                    NetworkInfo.State state = info.getState();
+
+                    if (VDBG || (state == NetworkInfo.State.CONNECTED) ||
+                            (state == NetworkInfo.State.DISCONNECTED) ||
+                            (state == NetworkInfo.State.SUSPENDED)) {
+                        log("ConnectivityChange for " +
+                            info.getTypeName() + ": " +
+                            state + "/" + info.getDetailedState());
+                    }
+
+                    // Since mobile has the notion of a network/apn that can be used for
+                    // provisioning we need to check every time we're connected as
+                    // CaptiveProtalTracker won't detected it because DCT doesn't report it
+                    // as connected as ACTION_ANY_DATA_CONNECTION_STATE_CHANGED instead its
+                    // reported as ACTION_DATA_CONNECTION_CONNECTED_TO_PROVISIONING_APN. Which
+                    // is received by MDST and sent here as EVENT_STATE_CHANGED.
+                    if (ConnectivityManager.isNetworkTypeMobile(info.getType())
+                            && (0 != Settings.Global.getInt(mContext.getContentResolver(),
+                                        Settings.Global.DEVICE_PROVISIONED, 0))
+                            && (((state == NetworkInfo.State.CONNECTED)
+                                    && (info.getType() == ConnectivityManager.TYPE_MOBILE))
+                                || info.isConnectedToProvisioningNetwork())) {
+                        log("ConnectivityChange checkMobileProvisioning for"
+                                + " TYPE_MOBILE or ProvisioningNetwork");
+                        checkMobileProvisioning(CheckMp.MAX_TIMEOUT_MS);
+                    }
+
+                    EventLogTags.writeConnectivityStateChanged(
+                            info.getType(), info.getSubtype(), info.getDetailedState().ordinal());
+
+                    if (info.isConnectedToProvisioningNetwork()) {
+                        /**
+                         * TODO: Create ConnectivityManager.TYPE_MOBILE_PROVISIONING
+                         * for now its an in between network, its a network that
+                         * is actually a default network but we don't want it to be
+                         * announced as such to keep background applications from
+                         * trying to use it. It turns out that some still try so we
+                         * take the additional step of clearing any default routes
+                         * to the link that may have incorrectly setup by the lower
+                         * levels.
+                         */
+                        LinkProperties lp = getLinkPropertiesForTypeInternal(info.getType());
+                        if (DBG) {
+                            log("EVENT_STATE_CHANGED: connected to provisioning network, lp=" + lp);
+                        }
+
+                        // Clear any default routes setup by the radio so
+                        // any activity by applications trying to use this
+                        // connection will fail until the provisioning network
+                        // is enabled.
+                        for (RouteInfo r : lp.getRoutes()) {
+                            removeRoute(lp, r, TO_DEFAULT_TABLE,
+                                        mNetTrackers[info.getType()].getNetwork().netId);
+                        }
+                    } else if (state == NetworkInfo.State.DISCONNECTED) {
+                    } else if (state == NetworkInfo.State.SUSPENDED) {
+                    } else if (state == NetworkInfo.State.CONNECTED) {
+                    //    handleConnect(info);
+                    }
+                    if (mLockdownTracker != null) {
+                        mLockdownTracker.onNetworkInfoChanged(info);
+                    }
+                    break;
+                }
+                case NetworkStateTracker.EVENT_CONFIGURATION_CHANGED: {
+                    info = (NetworkInfo) msg.obj;
+                    // TODO: Temporary allowing network configuration
+                    //       change not resetting sockets.
+                    //       @see bug/4455071
+                    handleConnectivityChange(info.getType(), mCurrentLinkProperties[info.getType()],
+                            false);
+                    break;
+                }
+                case NetworkStateTracker.EVENT_NETWORK_SUBTYPE_CHANGED: {
+                    info = (NetworkInfo) msg.obj;
+                    int type = info.getType();
+                    if (mNetConfigs[type].isDefault()) updateNetworkSettings(mNetTrackers[type]);
+                    break;
+                }
+            }
+        }
+    }
+
+    private void handleAsyncChannelHalfConnect(Message msg) {
+        AsyncChannel ac = (AsyncChannel) msg.obj;
+        if (mNetworkFactoryInfos.containsKey(msg.replyTo)) {
+            if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {
+                if (VDBG) log("NetworkFactory connected");
+                // A network factory has connected.  Send it all current NetworkRequests.
+                for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+                    if (nri.isRequest == false) continue;
+                    NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);
+                    ac.sendMessage(android.net.NetworkFactory.CMD_REQUEST_NETWORK,
+                            (nai != null ? nai.currentScore : 0), 0, nri.request);
+                }
+            } else {
+                loge("Error connecting NetworkFactory");
+                mNetworkFactoryInfos.remove(msg.obj);
+            }
+        } else if (mNetworkAgentInfos.containsKey(msg.replyTo)) {
+            if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {
+                if (VDBG) log("NetworkAgent connected");
+                // A network agent has requested a connection.  Establish the connection.
+                mNetworkAgentInfos.get(msg.replyTo).asyncChannel.
+                        sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
+            } else {
+                loge("Error connecting NetworkAgent");
+                NetworkAgentInfo nai = mNetworkAgentInfos.remove(msg.replyTo);
+                if (nai != null) {
+                    mNetworkForNetId.remove(nai.network.netId);
+                    mLegacyTypeTracker.remove(nai);
+                }
+            }
+        }
+    }
+    private void handleAsyncChannelDisconnected(Message msg) {
+        NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);
+        if (nai != null) {
+            if (DBG) {
+                log(nai.name() + " got DISCONNECTED, was satisfying " + nai.networkRequests.size());
+            }
+            // A network agent has disconnected.
+            // Tell netd to clean up the configuration for this network
+            // (routing rules, DNS, etc).
+            try {
+                mNetd.removeNetwork(nai.network.netId);
+            } catch (Exception e) {
+                loge("Exception removing network: " + e);
+            }
+            // TODO - if we move the logic to the network agent (have them disconnect
+            // because they lost all their requests or because their score isn't good)
+            // then they would disconnect organically, report their new state and then
+            // disconnect the channel.
+            if (nai.networkInfo.isConnected()) {
+                nai.networkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED,
+                        null, null);
+            }
+            notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOST);
+            nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_DISCONNECTED);
+            mNetworkAgentInfos.remove(msg.replyTo);
+            updateClat(null, nai.linkProperties, nai);
+            mLegacyTypeTracker.remove(nai);
+            mNetworkForNetId.remove(nai.network.netId);
+            // Since we've lost the network, go through all the requests that
+            // it was satisfying and see if any other factory can satisfy them.
+            final ArrayList<NetworkAgentInfo> toActivate = new ArrayList<NetworkAgentInfo>();
+            for (int i = 0; i < nai.networkRequests.size(); i++) {
+                NetworkRequest request = nai.networkRequests.valueAt(i);
+                NetworkAgentInfo currentNetwork = mNetworkForRequestId.get(request.requestId);
+                if (VDBG) {
+                    log(" checking request " + request + ", currentNetwork = " +
+                            (currentNetwork != null ? currentNetwork.name() : "null"));
+                }
+                if (currentNetwork != null && currentNetwork.network.netId == nai.network.netId) {
+                    mNetworkForRequestId.remove(request.requestId);
+                    sendUpdatedScoreToFactories(request, 0);
+                    NetworkAgentInfo alternative = null;
+                    for (Map.Entry entry : mNetworkAgentInfos.entrySet()) {
+                        NetworkAgentInfo existing = (NetworkAgentInfo)entry.getValue();
+                        if (existing.networkInfo.isConnected() &&
+                                request.networkCapabilities.satisfiedByNetworkCapabilities(
+                                existing.networkCapabilities) &&
+                                (alternative == null ||
+                                 alternative.currentScore < existing.currentScore)) {
+                            alternative = existing;
+                        }
+                    }
+                    if (alternative != null && !toActivate.contains(alternative)) {
+                        toActivate.add(alternative);
+                    }
+                }
+            }
+            if (nai.networkRequests.get(mDefaultRequest.requestId) != null) {
+                removeDataActivityTracking(nai);
+                mActiveDefaultNetwork = ConnectivityManager.TYPE_NONE;
+            }
+            for (NetworkAgentInfo networkToActivate : toActivate) {
+                networkToActivate.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_CONNECTED);
+            }
+        }
+    }
+
+    private void handleRegisterNetworkRequest(Message msg) {
+        final NetworkRequestInfo nri = (NetworkRequestInfo) (msg.obj);
+        final NetworkCapabilities newCap = nri.request.networkCapabilities;
+        int score = 0;
+
+        // Check for the best currently alive network that satisfies this request
+        NetworkAgentInfo bestNetwork = null;
+        for (NetworkAgentInfo network : mNetworkAgentInfos.values()) {
+            if (VDBG) log("handleRegisterNetworkRequest checking " + network.name());
+            if (newCap.satisfiedByNetworkCapabilities(network.networkCapabilities)) {
+                if (VDBG) log("apparently satisfied.  currentScore=" + network.currentScore);
+                if ((bestNetwork == null) || bestNetwork.currentScore < network.currentScore) {
+                    bestNetwork = network;
+                }
+            }
+        }
+        if (bestNetwork != null) {
+            if (VDBG) log("using " + bestNetwork.name());
+            bestNetwork.addRequest(nri.request);
+            mNetworkForRequestId.put(nri.request.requestId, bestNetwork);
+            int legacyType = nri.request.legacyType;
+            if (legacyType != TYPE_NONE) {
+                mLegacyTypeTracker.add(legacyType, bestNetwork);
+            }
+            notifyNetworkCallback(bestNetwork, nri);
+            score = bestNetwork.currentScore;
+        }
+        mNetworkRequests.put(nri.request, nri);
+        if (msg.what == EVENT_REGISTER_NETWORK_REQUEST) {
+            if (DBG) log("sending new NetworkRequest to factories");
+            for (NetworkFactoryInfo nfi : mNetworkFactoryInfos.values()) {
+                nfi.asyncChannel.sendMessage(android.net.NetworkFactory.CMD_REQUEST_NETWORK, score,
+                        0, nri.request);
+            }
+        }
+    }
+
+    private void handleReleaseNetworkRequest(NetworkRequest request) {
+        if (DBG) log("releasing NetworkRequest " + request);
+        NetworkRequestInfo nri = mNetworkRequests.remove(request);
+        if (nri != null) {
+            // tell the network currently servicing this that it's no longer interested
+            NetworkAgentInfo affectedNetwork = mNetworkForRequestId.get(nri.request.requestId);
+            if (affectedNetwork != null) {
+                mNetworkForRequestId.remove(nri.request.requestId);
+                affectedNetwork.networkRequests.remove(nri.request.requestId);
+                if (VDBG) {
+                    log(" Removing from current network " + affectedNetwork.name() + ", leaving " +
+                            affectedNetwork.networkRequests.size() + " requests.");
+                }
+            }
+
+            if (nri.isRequest) {
+                for (NetworkFactoryInfo nfi : mNetworkFactoryInfos.values()) {
+                    nfi.asyncChannel.sendMessage(android.net.NetworkFactory.CMD_CANCEL_REQUEST,
+                            nri.request);
+                }
+
+                if (affectedNetwork != null) {
+                    // check if this network still has live requests - otherwise, tear down
+                    // TODO - probably push this to the NF/NA
+                    boolean keep = false;
+                    for (int i = 0; i < affectedNetwork.networkRequests.size(); i++) {
+                        NetworkRequest r = affectedNetwork.networkRequests.valueAt(i);
+                        if (mNetworkRequests.get(r).isRequest) {
+                            keep = true;
+                            break;
+                        }
+                    }
+                    if (keep == false) {
+                        if (DBG) log("no live requests for " + affectedNetwork.name() +
+                                "; disconnecting");
+                        affectedNetwork.asyncChannel.disconnect();
+                    }
+                }
+            }
+            callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_RELEASED);
+        }
+    }
+
+    private class InternalHandler extends Handler {
+        public InternalHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            NetworkInfo info;
+            switch (msg.what) {
+                case EVENT_CLEAR_NET_TRANSITION_WAKELOCK: {
+                    String causedBy = null;
+                    synchronized (ConnectivityService.this) {
+                        if (msg.arg1 == mNetTransitionWakeLockSerialNumber &&
+                                mNetTransitionWakeLock.isHeld()) {
+                            mNetTransitionWakeLock.release();
+                            causedBy = mNetTransitionWakeLockCausedBy;
+                        }
+                    }
+                    if (causedBy != null) {
+                        log("NetTransition Wakelock for " + causedBy + " released by timeout");
+                    }
+                    break;
+                }
+                case EVENT_RESTORE_DEFAULT_NETWORK: {
+                    FeatureUser u = (FeatureUser)msg.obj;
+                    u.expire();
+                    break;
+                }
+                case EVENT_INET_CONDITION_CHANGE: {
+                    int netType = msg.arg1;
+                    int condition = msg.arg2;
+                    handleInetConditionChange(netType, condition);
+                    break;
+                }
+                case EVENT_INET_CONDITION_HOLD_END: {
+                    int netType = msg.arg1;
+                    int sequence = msg.arg2;
+                    handleInetConditionHoldEnd(netType, sequence);
+                    break;
+                }
+                case EVENT_APPLY_GLOBAL_HTTP_PROXY: {
+                    handleDeprecatedGlobalHttpProxy();
+                    break;
+                }
+                case EVENT_SET_DEPENDENCY_MET: {
+                    boolean met = (msg.arg1 == ENABLED);
+                    handleSetDependencyMet(msg.arg2, met);
+                    break;
+                }
+                case EVENT_SEND_STICKY_BROADCAST_INTENT: {
+                    Intent intent = (Intent)msg.obj;
+                    sendStickyBroadcast(intent);
+                    break;
+                }
+                case EVENT_SET_POLICY_DATA_ENABLE: {
+                    final int networkType = msg.arg1;
+                    final boolean enabled = msg.arg2 == ENABLED;
+                    handleSetPolicyDataEnable(networkType, enabled);
+                    break;
+                }
+                case EVENT_VPN_STATE_CHANGED: {
+                    if (mLockdownTracker != null) {
+                        mLockdownTracker.onVpnStateChanged((NetworkInfo) msg.obj);
+                    }
+                    break;
+                }
+                case EVENT_ENABLE_FAIL_FAST_MOBILE_DATA: {
+                    int tag = mEnableFailFastMobileDataTag.get();
+                    if (msg.arg1 == tag) {
+                        MobileDataStateTracker mobileDst =
+                            (MobileDataStateTracker) mNetTrackers[ConnectivityManager.TYPE_MOBILE];
+                        if (mobileDst != null) {
+                            mobileDst.setEnableFailFastMobileData(msg.arg2);
+                        }
+                    } else {
+                        log("EVENT_ENABLE_FAIL_FAST_MOBILE_DATA: stale arg1:" + msg.arg1
+                                + " != tag:" + tag);
+                    }
+                    break;
+                }
+                case EVENT_SAMPLE_INTERVAL_ELAPSED: {
+                    handleNetworkSamplingTimeout();
+                    break;
+                }
+                case EVENT_PROXY_HAS_CHANGED: {
+                    handleApplyDefaultProxy((ProxyInfo)msg.obj);
+                    break;
+                }
+                case EVENT_REGISTER_NETWORK_FACTORY: {
+                    handleRegisterNetworkFactory((NetworkFactoryInfo)msg.obj);
+                    break;
+                }
+                case EVENT_UNREGISTER_NETWORK_FACTORY: {
+                    handleUnregisterNetworkFactory((Messenger)msg.obj);
+                    break;
+                }
+                case EVENT_REGISTER_NETWORK_AGENT: {
+                    handleRegisterNetworkAgent((NetworkAgentInfo)msg.obj);
+                    break;
+                }
+                case EVENT_REGISTER_NETWORK_REQUEST:
+                case EVENT_REGISTER_NETWORK_LISTENER: {
+                    handleRegisterNetworkRequest(msg);
+                    break;
+                }
+                case EVENT_RELEASE_NETWORK_REQUEST: {
+                    handleReleaseNetworkRequest((NetworkRequest) msg.obj);
+                    break;
+                }
+            }
+        }
+    }
+
+    // javadoc from interface
+    public int tether(String iface) {
+        enforceTetherChangePermission();
+
+        if (isTetheringSupported()) {
+            return mTethering.tether(iface);
+        } else {
+            return ConnectivityManager.TETHER_ERROR_UNSUPPORTED;
+        }
+    }
+
+    // javadoc from interface
+    public int untether(String iface) {
+        enforceTetherChangePermission();
+
+        if (isTetheringSupported()) {
+            return mTethering.untether(iface);
+        } else {
+            return ConnectivityManager.TETHER_ERROR_UNSUPPORTED;
+        }
+    }
+
+    // javadoc from interface
+    public int getLastTetherError(String iface) {
+        enforceTetherAccessPermission();
+
+        if (isTetheringSupported()) {
+            return mTethering.getLastTetherError(iface);
+        } else {
+            return ConnectivityManager.TETHER_ERROR_UNSUPPORTED;
+        }
+    }
+
+    // TODO - proper iface API for selection by property, inspection, etc
+    public String[] getTetherableUsbRegexs() {
+        enforceTetherAccessPermission();
+        if (isTetheringSupported()) {
+            return mTethering.getTetherableUsbRegexs();
+        } else {
+            return new String[0];
+        }
+    }
+
+    public String[] getTetherableWifiRegexs() {
+        enforceTetherAccessPermission();
+        if (isTetheringSupported()) {
+            return mTethering.getTetherableWifiRegexs();
+        } else {
+            return new String[0];
+        }
+    }
+
+    public String[] getTetherableBluetoothRegexs() {
+        enforceTetherAccessPermission();
+        if (isTetheringSupported()) {
+            return mTethering.getTetherableBluetoothRegexs();
+        } else {
+            return new String[0];
+        }
+    }
+
+    public int setUsbTethering(boolean enable) {
+        enforceTetherChangePermission();
+        if (isTetheringSupported()) {
+            return mTethering.setUsbTethering(enable);
+        } else {
+            return ConnectivityManager.TETHER_ERROR_UNSUPPORTED;
+        }
+    }
+
+    // TODO - move iface listing, queries, etc to new module
+    // javadoc from interface
+    public String[] getTetherableIfaces() {
+        enforceTetherAccessPermission();
+        return mTethering.getTetherableIfaces();
+    }
+
+    public String[] getTetheredIfaces() {
+        enforceTetherAccessPermission();
+        return mTethering.getTetheredIfaces();
+    }
+
+    public String[] getTetheringErroredIfaces() {
+        enforceTetherAccessPermission();
+        return mTethering.getErroredIfaces();
+    }
+
+    // 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.
+    public boolean isTetheringSupported() {
+        enforceTetherAccessPermission();
+        int defaultVal = (SystemProperties.get("ro.tether.denied").equals("true") ? 0 : 1);
+        boolean tetherEnabledInSettings = (Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.TETHER_SUPPORTED, defaultVal) != 0);
+        return tetherEnabledInSettings && ((mTethering.getTetherableUsbRegexs().length != 0 ||
+                mTethering.getTetherableWifiRegexs().length != 0 ||
+                mTethering.getTetherableBluetoothRegexs().length != 0) &&
+                mTethering.getUpstreamIfaceTypes().length != 0);
+    }
+
+    // An API NetworkStateTrackers can call when they lose their network.
+    // This will automatically be cleared after X seconds or a network becomes CONNECTED,
+    // whichever happens first.  The timer is started by the first caller and not
+    // restarted by subsequent callers.
+    public void requestNetworkTransitionWakelock(String forWhom) {
+        enforceConnectivityInternalPermission();
+        synchronized (this) {
+            if (mNetTransitionWakeLock.isHeld()) return;
+            mNetTransitionWakeLockSerialNumber++;
+            mNetTransitionWakeLock.acquire();
+            mNetTransitionWakeLockCausedBy = forWhom;
+        }
+        mHandler.sendMessageDelayed(mHandler.obtainMessage(
+                EVENT_CLEAR_NET_TRANSITION_WAKELOCK,
+                mNetTransitionWakeLockSerialNumber, 0),
+                mNetTransitionWakeLockTimeout);
+        return;
+    }
+
+    // 100 percent is full good, 0 is full bad.
+    public void reportInetCondition(int networkType, int percentage) {
+        if (VDBG) log("reportNetworkCondition(" + networkType + ", " + percentage + ")");
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.STATUS_BAR,
+                "ConnectivityService");
+
+        if (DBG) {
+            int pid = getCallingPid();
+            int uid = getCallingUid();
+            String s = pid + "(" + uid + ") reports inet is " +
+                (percentage > 50 ? "connected" : "disconnected") + " (" + percentage + ") on " +
+                "network Type " + networkType + " at " + GregorianCalendar.getInstance().getTime();
+            mInetLog.add(s);
+            while(mInetLog.size() > INET_CONDITION_LOG_MAX_SIZE) {
+                mInetLog.remove(0);
+            }
+        }
+        mHandler.sendMessage(mHandler.obtainMessage(
+            EVENT_INET_CONDITION_CHANGE, networkType, percentage));
+    }
+
+    public void reportBadNetwork(Network network) {
+        //TODO
+    }
+
+    private void handleInetConditionChange(int netType, int condition) {
+        if (mActiveDefaultNetwork == -1) {
+            if (DBG) log("handleInetConditionChange: no active default network - ignore");
+            return;
+        }
+        if (mActiveDefaultNetwork != netType) {
+            if (DBG) log("handleInetConditionChange: net=" + netType +
+                            " != default=" + mActiveDefaultNetwork + " - ignore");
+            return;
+        }
+        if (VDBG) {
+            log("handleInetConditionChange: net=" +
+                    netType + ", condition=" + condition +
+                    ",mActiveDefaultNetwork=" + mActiveDefaultNetwork);
+        }
+        mDefaultInetCondition = condition;
+        int delay;
+        if (mInetConditionChangeInFlight == false) {
+            if (VDBG) log("handleInetConditionChange: starting a change hold");
+            // setup a new hold to debounce this
+            if (mDefaultInetCondition > 50) {
+                delay = Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.INET_CONDITION_DEBOUNCE_UP_DELAY, 500);
+            } else {
+                delay = Settings.Global.getInt(mContext.getContentResolver(),
+                        Settings.Global.INET_CONDITION_DEBOUNCE_DOWN_DELAY, 3000);
+            }
+            mInetConditionChangeInFlight = true;
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_INET_CONDITION_HOLD_END,
+                    mActiveDefaultNetwork, mDefaultConnectionSequence), delay);
+        } else {
+            // we've set the new condition, when this hold ends that will get picked up
+            if (VDBG) log("handleInetConditionChange: currently in hold - not setting new end evt");
+        }
+    }
+
+    private void handleInetConditionHoldEnd(int netType, int sequence) {
+        if (DBG) {
+            log("handleInetConditionHoldEnd: net=" + netType +
+                    ", condition=" + mDefaultInetCondition +
+                    ", published condition=" + mDefaultInetConditionPublished);
+        }
+        mInetConditionChangeInFlight = false;
+
+        if (mActiveDefaultNetwork == -1) {
+            if (DBG) log("handleInetConditionHoldEnd: no active default network - ignoring");
+            return;
+        }
+        if (mDefaultConnectionSequence != sequence) {
+            if (DBG) log("handleInetConditionHoldEnd: event hold for obsolete network - ignoring");
+            return;
+        }
+        // TODO: Figure out why this optimization sometimes causes a
+        //       change in mDefaultInetCondition to be missed and the
+        //       UI to not be updated.
+        //if (mDefaultInetConditionPublished == mDefaultInetCondition) {
+        //    if (DBG) log("no change in condition - aborting");
+        //    return;
+        //}
+        NetworkInfo networkInfo = getNetworkInfoForType(mActiveDefaultNetwork);
+        if (networkInfo.isConnected() == false) {
+            if (DBG) log("handleInetConditionHoldEnd: default network not connected - ignoring");
+            return;
+        }
+        mDefaultInetConditionPublished = mDefaultInetCondition;
+        sendInetConditionBroadcast(networkInfo);
+        return;
+    }
+
+    public ProxyInfo getProxy() {
+        // this information is already available as a world read/writable jvm property
+        // so this API change wouldn't have a benifit.  It also breaks the passing
+        // of proxy info to all the JVMs.
+        // enforceAccessPermission();
+        synchronized (mProxyLock) {
+            ProxyInfo ret = mGlobalProxy;
+            if ((ret == null) && !mDefaultProxyDisabled) ret = mDefaultProxy;
+            return ret;
+        }
+    }
+
+    public void setGlobalProxy(ProxyInfo proxyProperties) {
+        enforceConnectivityInternalPermission();
+
+        synchronized (mProxyLock) {
+            if (proxyProperties == mGlobalProxy) return;
+            if (proxyProperties != null && proxyProperties.equals(mGlobalProxy)) return;
+            if (mGlobalProxy != null && mGlobalProxy.equals(proxyProperties)) return;
+
+            String host = "";
+            int port = 0;
+            String exclList = "";
+            String pacFileUrl = "";
+            if (proxyProperties != null && (!TextUtils.isEmpty(proxyProperties.getHost()) ||
+                    (proxyProperties.getPacFileUrl() != null))) {
+                if (!proxyProperties.isValid()) {
+                    if (DBG)
+                        log("Invalid proxy properties, ignoring: " + proxyProperties.toString());
+                    return;
+                }
+                mGlobalProxy = new ProxyInfo(proxyProperties);
+                host = mGlobalProxy.getHost();
+                port = mGlobalProxy.getPort();
+                exclList = mGlobalProxy.getExclusionListAsString();
+                if (proxyProperties.getPacFileUrl() != null) {
+                    pacFileUrl = proxyProperties.getPacFileUrl().toString();
+                }
+            } else {
+                mGlobalProxy = null;
+            }
+            ContentResolver res = mContext.getContentResolver();
+            final long token = Binder.clearCallingIdentity();
+            try {
+                Settings.Global.putString(res, Settings.Global.GLOBAL_HTTP_PROXY_HOST, host);
+                Settings.Global.putInt(res, Settings.Global.GLOBAL_HTTP_PROXY_PORT, port);
+                Settings.Global.putString(res, Settings.Global.GLOBAL_HTTP_PROXY_EXCLUSION_LIST,
+                        exclList);
+                Settings.Global.putString(res, Settings.Global.GLOBAL_HTTP_PROXY_PAC, pacFileUrl);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        if (mGlobalProxy == null) {
+            proxyProperties = mDefaultProxy;
+        }
+        sendProxyBroadcast(proxyProperties);
+    }
+
+    private void loadGlobalProxy() {
+        ContentResolver res = mContext.getContentResolver();
+        String host = Settings.Global.getString(res, Settings.Global.GLOBAL_HTTP_PROXY_HOST);
+        int port = Settings.Global.getInt(res, Settings.Global.GLOBAL_HTTP_PROXY_PORT, 0);
+        String exclList = Settings.Global.getString(res,
+                Settings.Global.GLOBAL_HTTP_PROXY_EXCLUSION_LIST);
+        String pacFileUrl = Settings.Global.getString(res, Settings.Global.GLOBAL_HTTP_PROXY_PAC);
+        if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(pacFileUrl)) {
+            ProxyInfo proxyProperties;
+            if (!TextUtils.isEmpty(pacFileUrl)) {
+                proxyProperties = new ProxyInfo(pacFileUrl);
+            } else {
+                proxyProperties = new ProxyInfo(host, port, exclList);
+            }
+            if (!proxyProperties.isValid()) {
+                if (DBG) log("Invalid proxy properties, ignoring: " + proxyProperties.toString());
+                return;
+            }
+
+            synchronized (mProxyLock) {
+                mGlobalProxy = proxyProperties;
+            }
+        }
+    }
+
+    public ProxyInfo getGlobalProxy() {
+        // this information is already available as a world read/writable jvm property
+        // so this API change wouldn't have a benifit.  It also breaks the passing
+        // of proxy info to all the JVMs.
+        // enforceAccessPermission();
+        synchronized (mProxyLock) {
+            return mGlobalProxy;
+        }
+    }
+
+    private void handleApplyDefaultProxy(ProxyInfo proxy) {
+        if (proxy != null && TextUtils.isEmpty(proxy.getHost())
+                && (proxy.getPacFileUrl() == null)) {
+            proxy = null;
+        }
+        synchronized (mProxyLock) {
+            if (mDefaultProxy != null && mDefaultProxy.equals(proxy)) return;
+            if (mDefaultProxy == proxy) return; // catches repeated nulls
+            if (proxy != null &&  !proxy.isValid()) {
+                if (DBG) log("Invalid proxy properties, ignoring: " + proxy.toString());
+                return;
+            }
+
+            // This call could be coming from the PacManager, containing the port of the local
+            // proxy.  If this new proxy matches the global proxy then copy this proxy to the
+            // global (to get the correct local port), and send a broadcast.
+            // TODO: Switch PacManager to have its own message to send back rather than
+            // reusing EVENT_HAS_CHANGED_PROXY and this call to handleApplyDefaultProxy.
+            if ((mGlobalProxy != null) && (proxy != null) && (proxy.getPacFileUrl() != null)
+                    && proxy.getPacFileUrl().equals(mGlobalProxy.getPacFileUrl())) {
+                mGlobalProxy = proxy;
+                sendProxyBroadcast(mGlobalProxy);
+                return;
+            }
+            mDefaultProxy = proxy;
+
+            if (mGlobalProxy != null) return;
+            if (!mDefaultProxyDisabled) {
+                sendProxyBroadcast(proxy);
+            }
+        }
+    }
+
+    private void handleDeprecatedGlobalHttpProxy() {
+        String proxy = Settings.Global.getString(mContext.getContentResolver(),
+                Settings.Global.HTTP_PROXY);
+        if (!TextUtils.isEmpty(proxy)) {
+            String data[] = proxy.split(":");
+            if (data.length == 0) {
+                return;
+            }
+
+            String proxyHost =  data[0];
+            int proxyPort = 8080;
+            if (data.length > 1) {
+                try {
+                    proxyPort = Integer.parseInt(data[1]);
+                } catch (NumberFormatException e) {
+                    return;
+                }
+            }
+            ProxyInfo p = new ProxyInfo(data[0], proxyPort, "");
+            setGlobalProxy(p);
+        }
+    }
+
+    private void sendProxyBroadcast(ProxyInfo proxy) {
+        if (proxy == null) proxy = new ProxyInfo("", 0, "");
+        if (mPacManager.setCurrentProxyScriptUrl(proxy)) return;
+        if (DBG) log("sending Proxy Broadcast for " + proxy);
+        Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
+        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING |
+            Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        intent.putExtra(Proxy.EXTRA_PROXY_INFO, proxy);
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private static class SettingsObserver extends ContentObserver {
+        private int mWhat;
+        private Handler mHandler;
+        SettingsObserver(Handler handler, int what) {
+            super(handler);
+            mHandler = handler;
+            mWhat = what;
+        }
+
+        void observe(Context context) {
+            ContentResolver resolver = context.getContentResolver();
+            resolver.registerContentObserver(Settings.Global.getUriFor(
+                    Settings.Global.HTTP_PROXY), false, this);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mHandler.obtainMessage(mWhat).sendToTarget();
+        }
+    }
+
+    private static void log(String s) {
+        Slog.d(TAG, s);
+    }
+
+    private static void loge(String s) {
+        Slog.e(TAG, s);
+    }
+
+    int convertFeatureToNetworkType(int networkType, String feature) {
+        int usedNetworkType = networkType;
+
+        if(networkType == ConnectivityManager.TYPE_MOBILE) {
+            if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_MMS)) {
+                usedNetworkType = ConnectivityManager.TYPE_MOBILE_MMS;
+            } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_SUPL)) {
+                usedNetworkType = ConnectivityManager.TYPE_MOBILE_SUPL;
+            } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_DUN) ||
+                    TextUtils.equals(feature, Phone.FEATURE_ENABLE_DUN_ALWAYS)) {
+                usedNetworkType = ConnectivityManager.TYPE_MOBILE_DUN;
+            } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_HIPRI)) {
+                usedNetworkType = ConnectivityManager.TYPE_MOBILE_HIPRI;
+            } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_FOTA)) {
+                usedNetworkType = ConnectivityManager.TYPE_MOBILE_FOTA;
+            } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_IMS)) {
+                usedNetworkType = ConnectivityManager.TYPE_MOBILE_IMS;
+            } else if (TextUtils.equals(feature, Phone.FEATURE_ENABLE_CBS)) {
+                usedNetworkType = ConnectivityManager.TYPE_MOBILE_CBS;
+            } else {
+                Slog.e(TAG, "Can't match any mobile netTracker!");
+            }
+        } else if (networkType == ConnectivityManager.TYPE_WIFI) {
+            if (TextUtils.equals(feature, "p2p")) {
+                usedNetworkType = ConnectivityManager.TYPE_WIFI_P2P;
+            } else {
+                Slog.e(TAG, "Can't match any wifi netTracker!");
+            }
+        } else {
+            Slog.e(TAG, "Unexpected network type");
+        }
+        return usedNetworkType;
+    }
+
+    private static <T> T checkNotNull(T value, String message) {
+        if (value == null) {
+            throw new NullPointerException(message);
+        }
+        return value;
+    }
+
+    /**
+     * Protect a socket from VPN routing rules. This method is used by
+     * VpnBuilder and not available in ConnectivityManager. Permissions
+     * are checked in Vpn class.
+     * @hide
+     */
+    @Override
+    public boolean protectVpn(ParcelFileDescriptor socket) {
+        throwIfLockdownEnabled();
+        try {
+            int type = mActiveDefaultNetwork;
+            int user = UserHandle.getUserId(Binder.getCallingUid());
+            if (ConnectivityManager.isNetworkTypeValid(type) && mNetTrackers[type] != null) {
+                synchronized(mVpns) {
+                    mVpns.get(user).protect(socket);
+                }
+                return true;
+            }
+        } catch (Exception e) {
+            // ignore
+        } finally {
+            try {
+                socket.close();
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Prepare for a VPN application. This method is used by VpnDialogs
+     * and not available in ConnectivityManager. Permissions are checked
+     * in Vpn class.
+     * @hide
+     */
+    @Override
+    public boolean prepareVpn(String oldPackage, String newPackage) {
+        throwIfLockdownEnabled();
+        int user = UserHandle.getUserId(Binder.getCallingUid());
+        synchronized(mVpns) {
+            return mVpns.get(user).prepare(oldPackage, newPackage);
+        }
+    }
+
+    @Override
+    public void markSocketAsUser(ParcelFileDescriptor socket, int uid) {
+        enforceMarkNetworkSocketPermission();
+        final long token = Binder.clearCallingIdentity();
+        try {
+            int mark = mNetd.getMarkForUid(uid);
+            // Clear the mark on the socket if no mark is needed to prevent socket reuse issues
+            if (mark == -1) {
+                mark = 0;
+            }
+            NetworkUtils.markSocket(socket.getFd(), mark);
+        } catch (RemoteException e) {
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Configure a TUN interface and return its file descriptor. Parameters
+     * are encoded and opaque to this class. This method is used by VpnBuilder
+     * and not available in ConnectivityManager. Permissions are checked in
+     * Vpn class.
+     * @hide
+     */
+    @Override
+    public ParcelFileDescriptor establishVpn(VpnConfig config) {
+        throwIfLockdownEnabled();
+        int user = UserHandle.getUserId(Binder.getCallingUid());
+        synchronized(mVpns) {
+            return mVpns.get(user).establish(config);
+        }
+    }
+
+    /**
+     * Start legacy VPN, controlling native daemons as needed. Creates a
+     * secondary thread to perform connection work, returning quickly.
+     */
+    @Override
+    public void startLegacyVpn(VpnProfile profile) {
+        throwIfLockdownEnabled();
+        final LinkProperties egress = getActiveLinkProperties();
+        if (egress == null) {
+            throw new IllegalStateException("Missing active network connection");
+        }
+        int user = UserHandle.getUserId(Binder.getCallingUid());
+        synchronized(mVpns) {
+            mVpns.get(user).startLegacyVpn(profile, mKeyStore, egress);
+        }
+    }
+
+    /**
+     * Return the information of the ongoing legacy VPN. This method is used
+     * by VpnSettings and not available in ConnectivityManager. Permissions
+     * are checked in Vpn class.
+     * @hide
+     */
+    @Override
+    public LegacyVpnInfo getLegacyVpnInfo() {
+        throwIfLockdownEnabled();
+        int user = UserHandle.getUserId(Binder.getCallingUid());
+        synchronized(mVpns) {
+            return mVpns.get(user).getLegacyVpnInfo();
+        }
+    }
+
+    /**
+     * Returns the information of the ongoing VPN. This method is used by VpnDialogs and
+     * not available in ConnectivityManager.
+     * Permissions are checked in Vpn class.
+     * @hide
+     */
+    @Override
+    public VpnConfig getVpnConfig() {
+        int user = UserHandle.getUserId(Binder.getCallingUid());
+        synchronized(mVpns) {
+            return mVpns.get(user).getVpnConfig();
+        }
+    }
+
+    /**
+     * Callback for VPN subsystem. Currently VPN is not adapted to the service
+     * through NetworkStateTracker since it works differently. For example, it
+     * needs to override DNS servers but never takes the default routes. It
+     * relies on another data network, and it could keep existing connections
+     * alive after reconnecting, switching between networks, or even resuming
+     * from deep sleep. Calls from applications should be done synchronously
+     * to avoid race conditions. As these are all hidden APIs, refactoring can
+     * be done whenever a better abstraction is developed.
+     */
+    public class VpnCallback {
+        private VpnCallback() {
+        }
+
+        public void onStateChanged(NetworkInfo info) {
+            mHandler.obtainMessage(EVENT_VPN_STATE_CHANGED, info).sendToTarget();
+        }
+
+        public void override(String iface, List<String> dnsServers, List<String> searchDomains) {
+            if (dnsServers == null) {
+                restore();
+                return;
+            }
+
+            // Convert DNS servers into addresses.
+            List<InetAddress> addresses = new ArrayList<InetAddress>();
+            for (String address : dnsServers) {
+                // Double check the addresses and remove invalid ones.
+                try {
+                    addresses.add(InetAddress.parseNumericAddress(address));
+                } catch (Exception e) {
+                    // ignore
+                }
+            }
+            if (addresses.isEmpty()) {
+                restore();
+                return;
+            }
+
+            // Concatenate search domains into a string.
+            StringBuilder buffer = new StringBuilder();
+            if (searchDomains != null) {
+                for (String domain : searchDomains) {
+                    buffer.append(domain).append(' ');
+                }
+            }
+            String domains = buffer.toString().trim();
+
+            // Apply DNS changes.
+            synchronized (mDnsLock) {
+                // TODO: Re-enable this when the netId of the VPN is known.
+                // updateDnsLocked("VPN", netId, addresses, domains);
+            }
+
+            // Temporarily disable the default proxy (not global).
+            synchronized (mProxyLock) {
+                mDefaultProxyDisabled = true;
+                if (mGlobalProxy == null && mDefaultProxy != null) {
+                    sendProxyBroadcast(null);
+                }
+            }
+
+            // TODO: support proxy per network.
+        }
+
+        public void restore() {
+            synchronized (mProxyLock) {
+                mDefaultProxyDisabled = false;
+                if (mGlobalProxy == null && mDefaultProxy != null) {
+                    sendProxyBroadcast(mDefaultProxy);
+                }
+            }
+        }
+
+        public void protect(ParcelFileDescriptor socket) {
+            try {
+                final int mark = mNetd.getMarkForProtect();
+                NetworkUtils.markSocket(socket.getFd(), mark);
+            } catch (RemoteException e) {
+            }
+        }
+
+        public void setRoutes(String interfaze, List<RouteInfo> routes) {
+            for (RouteInfo route : routes) {
+                try {
+                    mNetd.setMarkedForwardingRoute(interfaze, route);
+                } catch (RemoteException e) {
+                }
+            }
+        }
+
+        public void setMarkedForwarding(String interfaze) {
+            try {
+                mNetd.setMarkedForwarding(interfaze);
+            } catch (RemoteException e) {
+            }
+        }
+
+        public void clearMarkedForwarding(String interfaze) {
+            try {
+                mNetd.clearMarkedForwarding(interfaze);
+            } catch (RemoteException e) {
+            }
+        }
+
+        public void addUserForwarding(String interfaze, int uid, boolean forwardDns) {
+            int uidStart = uid * UserHandle.PER_USER_RANGE;
+            int uidEnd = uidStart + UserHandle.PER_USER_RANGE - 1;
+            addUidForwarding(interfaze, uidStart, uidEnd, forwardDns);
+        }
+
+        public void clearUserForwarding(String interfaze, int uid, boolean forwardDns) {
+            int uidStart = uid * UserHandle.PER_USER_RANGE;
+            int uidEnd = uidStart + UserHandle.PER_USER_RANGE - 1;
+            clearUidForwarding(interfaze, uidStart, uidEnd, forwardDns);
+        }
+
+        public void addUidForwarding(String interfaze, int uidStart, int uidEnd,
+                boolean forwardDns) {
+            // TODO: Re-enable this when the netId of the VPN is known.
+            // try {
+            //     mNetd.setUidRangeRoute(netId, uidStart, uidEnd, forwardDns);
+            // } catch (RemoteException e) {
+            // }
+
+        }
+
+        public void clearUidForwarding(String interfaze, int uidStart, int uidEnd,
+                boolean forwardDns) {
+            // TODO: Re-enable this when the netId of the VPN is known.
+            // try {
+            //     mNetd.clearUidRangeRoute(interfaze, uidStart, uidEnd);
+            // } catch (RemoteException e) {
+            // }
+
+        }
+    }
+
+    @Override
+    public boolean updateLockdownVpn() {
+        if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+            Slog.w(TAG, "Lockdown VPN only available to AID_SYSTEM");
+            return false;
+        }
+
+        // Tear down existing lockdown if profile was removed
+        mLockdownEnabled = LockdownVpnTracker.isEnabled();
+        if (mLockdownEnabled) {
+            if (!mKeyStore.isUnlocked()) {
+                Slog.w(TAG, "KeyStore locked; unable to create LockdownTracker");
+                return false;
+            }
+
+            final String profileName = new String(mKeyStore.get(Credentials.LOCKDOWN_VPN));
+            final VpnProfile profile = VpnProfile.decode(
+                    profileName, mKeyStore.get(Credentials.VPN + profileName));
+            int user = UserHandle.getUserId(Binder.getCallingUid());
+            synchronized(mVpns) {
+                setLockdownTracker(new LockdownVpnTracker(mContext, mNetd, this, mVpns.get(user),
+                            profile));
+            }
+        } else {
+            setLockdownTracker(null);
+        }
+
+        return true;
+    }
+
+    /**
+     * Internally set new {@link LockdownVpnTracker}, shutting down any existing
+     * {@link LockdownVpnTracker}. Can be {@code null} to disable lockdown.
+     */
+    private void setLockdownTracker(LockdownVpnTracker tracker) {
+        // Shutdown any existing tracker
+        final LockdownVpnTracker existing = mLockdownTracker;
+        mLockdownTracker = null;
+        if (existing != null) {
+            existing.shutdown();
+        }
+
+        try {
+            if (tracker != null) {
+                mNetd.setFirewallEnabled(true);
+                mNetd.setFirewallInterfaceRule("lo", true);
+                mLockdownTracker = tracker;
+                mLockdownTracker.init();
+            } else {
+                mNetd.setFirewallEnabled(false);
+            }
+        } catch (RemoteException e) {
+            // ignored; NMS lives inside system_server
+        }
+    }
+
+    private void throwIfLockdownEnabled() {
+        if (mLockdownEnabled) {
+            throw new IllegalStateException("Unavailable in lockdown mode");
+        }
+    }
+
+    public void supplyMessenger(int networkType, Messenger messenger) {
+        enforceConnectivityInternalPermission();
+
+        if (isNetworkTypeValid(networkType) && mNetTrackers[networkType] != null) {
+            mNetTrackers[networkType].supplyMessenger(messenger);
+        }
+    }
+
+    public int findConnectionTypeForIface(String iface) {
+        enforceConnectivityInternalPermission();
+
+        if (TextUtils.isEmpty(iface)) return ConnectivityManager.TYPE_NONE;
+        for (NetworkStateTracker tracker : mNetTrackers) {
+            if (tracker != null) {
+                LinkProperties lp = tracker.getLinkProperties();
+                if (lp != null && iface.equals(lp.getInterfaceName())) {
+                    return tracker.getNetworkInfo().getType();
+                }
+            }
+        }
+        return ConnectivityManager.TYPE_NONE;
+    }
+
+    /**
+     * Have mobile data fail fast if enabled.
+     *
+     * @param enabled DctConstants.ENABLED/DISABLED
+     */
+    private void setEnableFailFastMobileData(int enabled) {
+        int tag;
+
+        if (enabled == DctConstants.ENABLED) {
+            tag = mEnableFailFastMobileDataTag.incrementAndGet();
+        } else {
+            tag = mEnableFailFastMobileDataTag.get();
+        }
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_ENABLE_FAIL_FAST_MOBILE_DATA, tag,
+                         enabled));
+    }
+
+    private boolean isMobileDataStateTrackerReady() {
+        MobileDataStateTracker mdst =
+                (MobileDataStateTracker) mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI];
+        return (mdst != null) && (mdst.isReady());
+    }
+
+    /**
+     * The ResultReceiver resultCode for checkMobileProvisioning (CMP_RESULT_CODE)
+     */
+
+    /**
+     * No connection was possible to the network.
+     * This is NOT a warm sim.
+     */
+    private static final int CMP_RESULT_CODE_NO_CONNECTION = 0;
+
+    /**
+     * A connection was made to the internet, all is well.
+     * This is NOT a warm sim.
+     */
+    private static final int CMP_RESULT_CODE_CONNECTABLE = 1;
+
+    /**
+     * A connection was made but no dns server was available to resolve a name to address.
+     * This is NOT a warm sim since provisioning network is supported.
+     */
+    private static final int CMP_RESULT_CODE_NO_DNS = 2;
+
+    /**
+     * A connection was made but could not open a TCP connection.
+     * This is NOT a warm sim since provisioning network is supported.
+     */
+    private static final int CMP_RESULT_CODE_NO_TCP_CONNECTION = 3;
+
+    /**
+     * A connection was made but there was a redirection, we appear to be in walled garden.
+     * This is an indication of a warm sim on a mobile network such as T-Mobile.
+     */
+    private static final int CMP_RESULT_CODE_REDIRECTED = 4;
+
+    /**
+     * The mobile network is a provisioning network.
+     * This is an indication of a warm sim on a mobile network such as AT&T.
+     */
+    private static final int CMP_RESULT_CODE_PROVISIONING_NETWORK = 5;
+
+    /**
+     * The mobile network is provisioning
+     */
+    private static final int CMP_RESULT_CODE_IS_PROVISIONING = 6;
+
+    private AtomicBoolean mIsProvisioningNetwork = new AtomicBoolean(false);
+    private AtomicBoolean mIsStartingProvisioning = new AtomicBoolean(false);
+
+    private AtomicBoolean mIsCheckingMobileProvisioning = new AtomicBoolean(false);
+
+    @Override
+    public int checkMobileProvisioning(int suggestedTimeOutMs) {
+        int timeOutMs = -1;
+        if (DBG) log("checkMobileProvisioning: E suggestedTimeOutMs=" + suggestedTimeOutMs);
+        enforceConnectivityInternalPermission();
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            timeOutMs = suggestedTimeOutMs;
+            if (suggestedTimeOutMs > CheckMp.MAX_TIMEOUT_MS) {
+                timeOutMs = CheckMp.MAX_TIMEOUT_MS;
+            }
+
+            // Check that mobile networks are supported
+            if (!isNetworkSupported(ConnectivityManager.TYPE_MOBILE)
+                    || !isNetworkSupported(ConnectivityManager.TYPE_MOBILE_HIPRI)) {
+                if (DBG) log("checkMobileProvisioning: X no mobile network");
+                return timeOutMs;
+            }
+
+            // If we're already checking don't do it again
+            // TODO: Add a queue of results...
+            if (mIsCheckingMobileProvisioning.getAndSet(true)) {
+                if (DBG) log("checkMobileProvisioning: X already checking ignore for the moment");
+                return timeOutMs;
+            }
+
+            // Start off with mobile notification off
+            setProvNotificationVisible(false, ConnectivityManager.TYPE_MOBILE_HIPRI, null, null);
+
+            CheckMp checkMp = new CheckMp(mContext, this);
+            CheckMp.CallBack cb = new CheckMp.CallBack() {
+                @Override
+                void onComplete(Integer result) {
+                    if (DBG) log("CheckMp.onComplete: result=" + result);
+                    NetworkInfo ni =
+                            mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI].getNetworkInfo();
+                    switch(result) {
+                        case CMP_RESULT_CODE_CONNECTABLE:
+                        case CMP_RESULT_CODE_NO_CONNECTION:
+                        case CMP_RESULT_CODE_NO_DNS:
+                        case CMP_RESULT_CODE_NO_TCP_CONNECTION: {
+                            if (DBG) log("CheckMp.onComplete: ignore, connected or no connection");
+                            break;
+                        }
+                        case CMP_RESULT_CODE_REDIRECTED: {
+                            if (DBG) log("CheckMp.onComplete: warm sim");
+                            String url = getMobileProvisioningUrl();
+                            if (TextUtils.isEmpty(url)) {
+                                url = getMobileRedirectedProvisioningUrl();
+                            }
+                            if (TextUtils.isEmpty(url) == false) {
+                                if (DBG) log("CheckMp.onComplete: warm (redirected), url=" + url);
+                                setProvNotificationVisible(true,
+                                        ConnectivityManager.TYPE_MOBILE_HIPRI, ni.getExtraInfo(),
+                                        url);
+                            } else {
+                                if (DBG) log("CheckMp.onComplete: warm (redirected), no url");
+                            }
+                            break;
+                        }
+                        case CMP_RESULT_CODE_PROVISIONING_NETWORK: {
+                            String url = getMobileProvisioningUrl();
+                            if (TextUtils.isEmpty(url) == false) {
+                                if (DBG) log("CheckMp.onComplete: warm (no dns/tcp), url=" + url);
+                                setProvNotificationVisible(true,
+                                        ConnectivityManager.TYPE_MOBILE_HIPRI, ni.getExtraInfo(),
+                                        url);
+                                // Mark that we've got a provisioning network and
+                                // Disable Mobile Data until user actually starts provisioning.
+                                mIsProvisioningNetwork.set(true);
+                                MobileDataStateTracker mdst = (MobileDataStateTracker)
+                                        mNetTrackers[ConnectivityManager.TYPE_MOBILE];
+
+                                // Disable radio until user starts provisioning
+                                mdst.setRadio(false);
+                            } else {
+                                if (DBG) log("CheckMp.onComplete: warm (no dns/tcp), no url");
+                            }
+                            break;
+                        }
+                        case CMP_RESULT_CODE_IS_PROVISIONING: {
+                            // FIXME: Need to know when provisioning is done. Probably we can
+                            // check the completion status if successful we're done if we
+                            // "timedout" or still connected to provisioning APN turn off data?
+                            if (DBG) log("CheckMp.onComplete: provisioning started");
+                            mIsStartingProvisioning.set(false);
+                            break;
+                        }
+                        default: {
+                            loge("CheckMp.onComplete: ignore unexpected result=" + result);
+                            break;
+                        }
+                    }
+                    mIsCheckingMobileProvisioning.set(false);
+                }
+            };
+            CheckMp.Params params =
+                    new CheckMp.Params(checkMp.getDefaultUrl(), timeOutMs, cb);
+            if (DBG) log("checkMobileProvisioning: params=" + params);
+            // TODO: Reenable when calls to the now defunct
+            //       MobileDataStateTracker.isProvisioningNetwork() are removed.
+            //       This code should be moved to the Telephony code.
+            // checkMp.execute(params);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+            if (DBG) log("checkMobileProvisioning: X");
+        }
+        return timeOutMs;
+    }
+
+    static class CheckMp extends
+            AsyncTask<CheckMp.Params, Void, Integer> {
+        private static final String CHECKMP_TAG = "CheckMp";
+
+        // adb shell setprop persist.checkmp.testfailures 1 to enable testing failures
+        private static boolean mTestingFailures;
+
+        // Choosing 4 loops as half of them will use HTTPS and the other half HTTP
+        private static final int MAX_LOOPS = 4;
+
+        // Number of milli-seconds to complete all of the retires
+        public static final int MAX_TIMEOUT_MS =  60000;
+
+        // The socket should retry only 5 seconds, the default is longer
+        private static final int SOCKET_TIMEOUT_MS = 5000;
+
+        // Sleep time for network errors
+        private static final int NET_ERROR_SLEEP_SEC = 3;
+
+        // Sleep time for network route establishment
+        private static final int NET_ROUTE_ESTABLISHMENT_SLEEP_SEC = 3;
+
+        // Short sleep time for polling :(
+        private static final int POLLING_SLEEP_SEC = 1;
+
+        private Context mContext;
+        private ConnectivityService mCs;
+        private TelephonyManager mTm;
+        private Params mParams;
+
+        /**
+         * Parameters for AsyncTask.execute
+         */
+        static class Params {
+            private String mUrl;
+            private long mTimeOutMs;
+            private CallBack mCb;
+
+            Params(String url, long timeOutMs, CallBack cb) {
+                mUrl = url;
+                mTimeOutMs = timeOutMs;
+                mCb = cb;
+            }
+
+            @Override
+            public String toString() {
+                return "{" + " url=" + mUrl + " mTimeOutMs=" + mTimeOutMs + " mCb=" + mCb + "}";
+            }
+        }
+
+        // As explained to me by Brian Carlstrom and Kenny Root, Certificates can be
+        // issued by name or ip address, for Google its by name so when we construct
+        // this HostnameVerifier we'll pass the original Uri and use it to verify
+        // the host. If the host name in the original uril fails we'll test the
+        // hostname parameter just incase things change.
+        static class CheckMpHostnameVerifier implements HostnameVerifier {
+            Uri mOrgUri;
+
+            CheckMpHostnameVerifier(Uri orgUri) {
+                mOrgUri = orgUri;
+            }
+
+            @Override
+            public boolean verify(String hostname, SSLSession session) {
+                HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
+                String orgUriHost = mOrgUri.getHost();
+                boolean retVal = hv.verify(orgUriHost, session) || hv.verify(hostname, session);
+                if (DBG) {
+                    log("isMobileOk: hostnameVerify retVal=" + retVal + " hostname=" + hostname
+                        + " orgUriHost=" + orgUriHost);
+                }
+                return retVal;
+            }
+        }
+
+        /**
+         * The call back object passed in Params. onComplete will be called
+         * on the main thread.
+         */
+        abstract static class CallBack {
+            // Called on the main thread.
+            abstract void onComplete(Integer result);
+        }
+
+        public CheckMp(Context context, ConnectivityService cs) {
+            if (Build.IS_DEBUGGABLE) {
+                mTestingFailures =
+                        SystemProperties.getInt("persist.checkmp.testfailures", 0) == 1;
+            } else {
+                mTestingFailures = false;
+            }
+
+            mContext = context;
+            mCs = cs;
+
+            // Setup access to TelephonyService we'll be using.
+            mTm = (TelephonyManager) mContext.getSystemService(
+                    Context.TELEPHONY_SERVICE);
+        }
+
+        /**
+         * Get the default url to use for the test.
+         */
+        public String getDefaultUrl() {
+            // See http://go/clientsdns for usage approval
+            String server = Settings.Global.getString(mContext.getContentResolver(),
+                    Settings.Global.CAPTIVE_PORTAL_SERVER);
+            if (server == null) {
+                server = "clients3.google.com";
+            }
+            return "http://" + server + "/generate_204";
+        }
+
+        /**
+         * Detect if its possible to connect to the http url. DNS based detection techniques
+         * do not work at all hotspots. The best way to check is to perform a request to
+         * a known address that fetches the data we expect.
+         */
+        private synchronized Integer isMobileOk(Params params) {
+            Integer result = CMP_RESULT_CODE_NO_CONNECTION;
+            Uri orgUri = Uri.parse(params.mUrl);
+            Random rand = new Random();
+            mParams = params;
+
+            if (mCs.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false) {
+                result = CMP_RESULT_CODE_NO_CONNECTION;
+                log("isMobileOk: X not mobile capable result=" + result);
+                return result;
+            }
+
+            if (mCs.mIsStartingProvisioning.get()) {
+                result = CMP_RESULT_CODE_IS_PROVISIONING;
+                log("isMobileOk: X is provisioning result=" + result);
+                return result;
+            }
+
+            // See if we've already determined we've got a provisioning connection,
+            // if so we don't need to do anything active.
+            MobileDataStateTracker mdstDefault = (MobileDataStateTracker)
+                    mCs.mNetTrackers[ConnectivityManager.TYPE_MOBILE];
+            boolean isDefaultProvisioning = mdstDefault.isProvisioningNetwork();
+            log("isMobileOk: isDefaultProvisioning=" + isDefaultProvisioning);
+
+            MobileDataStateTracker mdstHipri = (MobileDataStateTracker)
+                    mCs.mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI];
+            boolean isHipriProvisioning = mdstHipri.isProvisioningNetwork();
+            log("isMobileOk: isHipriProvisioning=" + isHipriProvisioning);
+
+            if (isDefaultProvisioning || isHipriProvisioning) {
+                result = CMP_RESULT_CODE_PROVISIONING_NETWORK;
+                log("isMobileOk: X default || hipri is provisioning result=" + result);
+                return result;
+            }
+
+            try {
+                // Continue trying to connect until time has run out
+                long endTime = SystemClock.elapsedRealtime() + params.mTimeOutMs;
+
+                if (!mCs.isMobileDataStateTrackerReady()) {
+                    // Wait for MobileDataStateTracker to be ready.
+                    if (DBG) log("isMobileOk: mdst is not ready");
+                    while(SystemClock.elapsedRealtime() < endTime) {
+                        if (mCs.isMobileDataStateTrackerReady()) {
+                            // Enable fail fast as we'll do retries here and use a
+                            // hipri connection so the default connection stays active.
+                            if (DBG) log("isMobileOk: mdst ready, enable fail fast of mobile data");
+                            mCs.setEnableFailFastMobileData(DctConstants.ENABLED);
+                            break;
+                        }
+                        sleep(POLLING_SLEEP_SEC);
+                    }
+                }
+
+                log("isMobileOk: start hipri url=" + params.mUrl);
+
+                // First wait until we can start using hipri
+                Binder binder = new Binder();
+                while(SystemClock.elapsedRealtime() < endTime) {
+                    int ret = mCs.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE,
+                            Phone.FEATURE_ENABLE_HIPRI, binder);
+                    if ((ret == PhoneConstants.APN_ALREADY_ACTIVE)
+                        || (ret == PhoneConstants.APN_REQUEST_STARTED)) {
+                            log("isMobileOk: hipri started");
+                            break;
+                    }
+                    if (VDBG) log("isMobileOk: hipri not started yet");
+                    result = CMP_RESULT_CODE_NO_CONNECTION;
+                    sleep(POLLING_SLEEP_SEC);
+                }
+
+                // Continue trying to connect until time has run out
+                while(SystemClock.elapsedRealtime() < endTime) {
+                    try {
+                        // Wait for hipri to connect.
+                        // TODO: Don't poll and handle situation where hipri fails
+                        // because default is retrying. See b/9569540
+                        NetworkInfo.State state = mCs
+                                .getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI).getState();
+                        if (state != NetworkInfo.State.CONNECTED) {
+                            if (true/*VDBG*/) {
+                                log("isMobileOk: not connected ni=" +
+                                    mCs.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI));
+                            }
+                            sleep(POLLING_SLEEP_SEC);
+                            result = CMP_RESULT_CODE_NO_CONNECTION;
+                            continue;
+                        }
+
+                        // Hipri has started check if this is a provisioning url
+                        MobileDataStateTracker mdst = (MobileDataStateTracker)
+                                mCs.mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI];
+                        if (mdst.isProvisioningNetwork()) {
+                            result = CMP_RESULT_CODE_PROVISIONING_NETWORK;
+                            if (DBG) log("isMobileOk: X isProvisioningNetwork result=" + result);
+                            return result;
+                        } else {
+                            if (DBG) log("isMobileOk: isProvisioningNetwork is false, continue");
+                        }
+
+                        // Get of the addresses associated with the url host. We need to use the
+                        // address otherwise HttpURLConnection object will use the name to get
+                        // the addresses and will try every address but that will bypass the
+                        // route to host we setup and the connection could succeed as the default
+                        // interface might be connected to the internet via wifi or other interface.
+                        InetAddress[] addresses;
+                        try {
+                            addresses = InetAddress.getAllByName(orgUri.getHost());
+                        } catch (UnknownHostException e) {
+                            result = CMP_RESULT_CODE_NO_DNS;
+                            log("isMobileOk: X UnknownHostException result=" + result);
+                            return result;
+                        }
+                        log("isMobileOk: addresses=" + inetAddressesToString(addresses));
+
+                        // Get the type of addresses supported by this link
+                        LinkProperties lp = mCs.getLinkPropertiesForTypeInternal(
+                                ConnectivityManager.TYPE_MOBILE_HIPRI);
+                        boolean linkHasIpv4 = lp.hasIPv4Address();
+                        boolean linkHasIpv6 = lp.hasIPv6Address();
+                        log("isMobileOk: linkHasIpv4=" + linkHasIpv4
+                                + " linkHasIpv6=" + linkHasIpv6);
+
+                        final ArrayList<InetAddress> validAddresses =
+                                new ArrayList<InetAddress>(addresses.length);
+
+                        for (InetAddress addr : addresses) {
+                            if (((addr instanceof Inet4Address) && linkHasIpv4) ||
+                                    ((addr instanceof Inet6Address) && linkHasIpv6)) {
+                                validAddresses.add(addr);
+                            }
+                        }
+
+                        if (validAddresses.size() == 0) {
+                            return CMP_RESULT_CODE_NO_CONNECTION;
+                        }
+
+                        int addrTried = 0;
+                        while (true) {
+                            // Loop through at most MAX_LOOPS valid addresses or until
+                            // we run out of time
+                            if (addrTried++ >= MAX_LOOPS) {
+                                log("isMobileOk: too many loops tried - giving up");
+                                break;
+                            }
+                            if (SystemClock.elapsedRealtime() >= endTime) {
+                                log("isMobileOk: spend too much time - giving up");
+                                break;
+                            }
+
+                            InetAddress hostAddr = validAddresses.get(rand.nextInt(
+                                    validAddresses.size()));
+
+                            // Make a route to host so we check the specific interface.
+                            if (mCs.requestRouteToHostAddress(ConnectivityManager.TYPE_MOBILE_HIPRI,
+                                    hostAddr.getAddress(), null)) {
+                                // Wait a short time to be sure the route is established ??
+                                log("isMobileOk:"
+                                        + " wait to establish route to hostAddr=" + hostAddr);
+                                sleep(NET_ROUTE_ESTABLISHMENT_SLEEP_SEC);
+                            } else {
+                                log("isMobileOk:"
+                                        + " could not establish route to hostAddr=" + hostAddr);
+                                // Wait a short time before the next attempt
+                                sleep(NET_ERROR_SLEEP_SEC);
+                                continue;
+                            }
+
+                            // Rewrite the url to have numeric address to use the specific route
+                            // using http for half the attempts and https for the other half.
+                            // Doing https first and http second as on a redirected walled garden
+                            // such as t-mobile uses we get a SocketTimeoutException: "SSL
+                            // handshake timed out" which we declare as
+                            // CMP_RESULT_CODE_NO_TCP_CONNECTION. We could change this, but by
+                            // having http second we will be using logic used for some time.
+                            URL newUrl;
+                            String scheme = (addrTried <= (MAX_LOOPS/2)) ? "https" : "http";
+                            newUrl = new URL(scheme, hostAddr.getHostAddress(),
+                                        orgUri.getPath());
+                            log("isMobileOk: newUrl=" + newUrl);
+
+                            HttpURLConnection urlConn = null;
+                            try {
+                                // Open the connection set the request headers and get the response
+                                urlConn = (HttpURLConnection)newUrl.openConnection(
+                                        java.net.Proxy.NO_PROXY);
+                                if (scheme.equals("https")) {
+                                    ((HttpsURLConnection)urlConn).setHostnameVerifier(
+                                            new CheckMpHostnameVerifier(orgUri));
+                                }
+                                urlConn.setInstanceFollowRedirects(false);
+                                urlConn.setConnectTimeout(SOCKET_TIMEOUT_MS);
+                                urlConn.setReadTimeout(SOCKET_TIMEOUT_MS);
+                                urlConn.setUseCaches(false);
+                                urlConn.setAllowUserInteraction(false);
+                                // Set the "Connection" to "Close" as by default "Keep-Alive"
+                                // is used which is useless in this case.
+                                urlConn.setRequestProperty("Connection", "close");
+                                int responseCode = urlConn.getResponseCode();
+
+                                // For debug display the headers
+                                Map<String, List<String>> headers = urlConn.getHeaderFields();
+                                log("isMobileOk: headers=" + headers);
+
+                                // Close the connection
+                                urlConn.disconnect();
+                                urlConn = null;
+
+                                if (mTestingFailures) {
+                                    // Pretend no connection, this tests using http and https
+                                    result = CMP_RESULT_CODE_NO_CONNECTION;
+                                    log("isMobileOk: TESTING_FAILURES, pretend no connction");
+                                    continue;
+                                }
+
+                                if (responseCode == 204) {
+                                    // Return
+                                    result = CMP_RESULT_CODE_CONNECTABLE;
+                                    log("isMobileOk: X got expected responseCode=" + responseCode
+                                            + " result=" + result);
+                                    return result;
+                                } else {
+                                    // Retry to be sure this was redirected, we've gotten
+                                    // occasions where a server returned 200 even though
+                                    // the device didn't have a "warm" sim.
+                                    log("isMobileOk: not expected responseCode=" + responseCode);
+                                    // TODO - it would be nice in the single-address case to do
+                                    // another DNS resolve here, but flushing the cache is a bit
+                                    // heavy-handed.
+                                    result = CMP_RESULT_CODE_REDIRECTED;
+                                }
+                            } catch (Exception e) {
+                                log("isMobileOk: HttpURLConnection Exception" + e);
+                                result = CMP_RESULT_CODE_NO_TCP_CONNECTION;
+                                if (urlConn != null) {
+                                    urlConn.disconnect();
+                                    urlConn = null;
+                                }
+                                sleep(NET_ERROR_SLEEP_SEC);
+                                continue;
+                            }
+                        }
+                        log("isMobileOk: X loops|timed out result=" + result);
+                        return result;
+                    } catch (Exception e) {
+                        log("isMobileOk: Exception e=" + e);
+                        continue;
+                    }
+                }
+                log("isMobileOk: timed out");
+            } finally {
+                log("isMobileOk: F stop hipri");
+                mCs.setEnableFailFastMobileData(DctConstants.DISABLED);
+                mCs.stopUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE,
+                        Phone.FEATURE_ENABLE_HIPRI);
+
+                // Wait for hipri to disconnect.
+                long endTime = SystemClock.elapsedRealtime() + 5000;
+
+                while(SystemClock.elapsedRealtime() < endTime) {
+                    NetworkInfo.State state = mCs
+                            .getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI).getState();
+                    if (state != NetworkInfo.State.DISCONNECTED) {
+                        if (VDBG) {
+                            log("isMobileOk: connected ni=" +
+                                mCs.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI));
+                        }
+                        sleep(POLLING_SLEEP_SEC);
+                        continue;
+                    }
+                }
+
+                log("isMobileOk: X result=" + result);
+            }
+            return result;
+        }
+
+        @Override
+        protected Integer doInBackground(Params... params) {
+            return isMobileOk(params[0]);
+        }
+
+        @Override
+        protected void onPostExecute(Integer result) {
+            log("onPostExecute: result=" + result);
+            if ((mParams != null) && (mParams.mCb != null)) {
+                mParams.mCb.onComplete(result);
+            }
+        }
+
+        private String inetAddressesToString(InetAddress[] addresses) {
+            StringBuffer sb = new StringBuffer();
+            boolean firstTime = true;
+            for(InetAddress addr : addresses) {
+                if (firstTime) {
+                    firstTime = false;
+                } else {
+                    sb.append(",");
+                }
+                sb.append(addr);
+            }
+            return sb.toString();
+        }
+
+        private void printNetworkInfo() {
+            boolean hasIccCard = mTm.hasIccCard();
+            int simState = mTm.getSimState();
+            log("hasIccCard=" + hasIccCard
+                    + " simState=" + simState);
+            NetworkInfo[] ni = mCs.getAllNetworkInfo();
+            if (ni != null) {
+                log("ni.length=" + ni.length);
+                for (NetworkInfo netInfo: ni) {
+                    log("netInfo=" + netInfo.toString());
+                }
+            } else {
+                log("no network info ni=null");
+            }
+        }
+
+        /**
+         * Sleep for a few seconds then return.
+         * @param seconds
+         */
+        private static void sleep(int seconds) {
+            long stopTime = System.nanoTime() + (seconds * 1000000000);
+            long sleepTime;
+            while ((sleepTime = stopTime - System.nanoTime()) > 0) {
+                try {
+                    Thread.sleep(sleepTime / 1000000);
+                } catch (InterruptedException ignored) {
+                }
+            }
+        }
+
+        private static void log(String s) {
+            Slog.d(ConnectivityService.TAG, "[" + CHECKMP_TAG + "] " + s);
+        }
+    }
+
+    // TODO: Move to ConnectivityManager and make public?
+    private static final String CONNECTED_TO_PROVISIONING_NETWORK_ACTION =
+            "com.android.server.connectivityservice.CONNECTED_TO_PROVISIONING_NETWORK_ACTION";
+
+    private BroadcastReceiver mProvisioningReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(CONNECTED_TO_PROVISIONING_NETWORK_ACTION)) {
+                handleMobileProvisioningAction(intent.getStringExtra("EXTRA_URL"));
+            }
+        }
+    };
+
+    private void handleMobileProvisioningAction(String url) {
+        // Mark notification as not visible
+        setProvNotificationVisible(false, ConnectivityManager.TYPE_MOBILE_HIPRI, null, null);
+
+        // Check airplane mode
+        boolean isAirplaneModeOn = Settings.System.getInt(mContext.getContentResolver(),
+                Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
+        // If provisioning network and not in airplane mode handle as a special case,
+        // otherwise launch browser with the intent directly.
+        if (mIsProvisioningNetwork.get() && !isAirplaneModeOn) {
+            if (DBG) log("handleMobileProvisioningAction: on prov network enable then launch");
+            mIsProvisioningNetwork.set(false);
+//            mIsStartingProvisioning.set(true);
+//            MobileDataStateTracker mdst = (MobileDataStateTracker)
+//                    mNetTrackers[ConnectivityManager.TYPE_MOBILE];
+            // Radio was disabled on CMP_RESULT_CODE_PROVISIONING_NETWORK, enable it here
+//            mdst.setRadio(true);
+//            mdst.setEnableFailFastMobileData(DctConstants.ENABLED);
+//            mdst.enableMobileProvisioning(url);
+        } else {
+            if (DBG) log("handleMobileProvisioningAction: not prov network");
+            mIsProvisioningNetwork.set(false);
+            // Check for  apps that can handle provisioning first
+            Intent provisioningIntent = new Intent(TelephonyIntents.ACTION_CARRIER_SETUP);
+            provisioningIntent.addCategory(TelephonyIntents.CATEGORY_MCCMNC_PREFIX
+                    + mTelephonyManager.getSimOperator());
+            if (mContext.getPackageManager().resolveActivity(provisioningIntent, 0 /* flags */)
+                    != null) {
+                provisioningIntent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
+                        Intent.FLAG_ACTIVITY_NEW_TASK);
+                mContext.startActivity(provisioningIntent);
+            } else {
+                // If no apps exist, use standard URL ACTION_VIEW method
+                Intent newIntent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN,
+                        Intent.CATEGORY_APP_BROWSER);
+                newIntent.setData(Uri.parse(url));
+                newIntent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
+                        Intent.FLAG_ACTIVITY_NEW_TASK);
+                try {
+                    mContext.startActivity(newIntent);
+                } catch (ActivityNotFoundException e) {
+                    loge("handleMobileProvisioningAction: startActivity failed" + e);
+                }
+            }
+        }
+    }
+
+    private static final String NOTIFICATION_ID = "CaptivePortal.Notification";
+    private volatile boolean mIsNotificationVisible = false;
+
+    private void setProvNotificationVisible(boolean visible, int networkType, String extraInfo,
+            String url) {
+        if (DBG) {
+            log("setProvNotificationVisible: E visible=" + visible + " networkType=" + networkType
+                + " extraInfo=" + extraInfo + " url=" + url);
+        }
+
+        Resources r = Resources.getSystem();
+        NotificationManager notificationManager = (NotificationManager) mContext
+            .getSystemService(Context.NOTIFICATION_SERVICE);
+
+        if (visible) {
+            CharSequence title;
+            CharSequence details;
+            int icon;
+            Intent intent;
+            Notification notification = new Notification();
+            switch (networkType) {
+                case ConnectivityManager.TYPE_WIFI:
+                    title = r.getString(R.string.wifi_available_sign_in, 0);
+                    details = r.getString(R.string.network_available_sign_in_detailed,
+                            extraInfo);
+                    icon = R.drawable.stat_notify_wifi_in_range;
+                    intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+                    intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
+                            Intent.FLAG_ACTIVITY_NEW_TASK);
+                    notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+                    break;
+                case ConnectivityManager.TYPE_MOBILE:
+                case ConnectivityManager.TYPE_MOBILE_HIPRI:
+                    title = r.getString(R.string.network_available_sign_in, 0);
+                    // TODO: Change this to pull from NetworkInfo once a printable
+                    // name has been added to it
+                    details = mTelephonyManager.getNetworkOperatorName();
+                    icon = R.drawable.stat_notify_rssi_in_range;
+                    intent = new Intent(CONNECTED_TO_PROVISIONING_NETWORK_ACTION);
+                    intent.putExtra("EXTRA_URL", url);
+                    intent.setFlags(0);
+                    notification.contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+                    break;
+                default:
+                    title = r.getString(R.string.network_available_sign_in, 0);
+                    details = r.getString(R.string.network_available_sign_in_detailed,
+                            extraInfo);
+                    icon = R.drawable.stat_notify_rssi_in_range;
+                    intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+                    intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
+                            Intent.FLAG_ACTIVITY_NEW_TASK);
+                    notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+                    break;
+            }
+
+            notification.when = 0;
+            notification.icon = icon;
+            notification.flags = Notification.FLAG_AUTO_CANCEL;
+            notification.tickerText = title;
+            notification.setLatestEventInfo(mContext, title, details, notification.contentIntent);
+
+            try {
+                notificationManager.notify(NOTIFICATION_ID, networkType, notification);
+            } catch (NullPointerException npe) {
+                loge("setNotificaitionVisible: visible notificationManager npe=" + npe);
+                npe.printStackTrace();
+            }
+        } else {
+            try {
+                notificationManager.cancel(NOTIFICATION_ID, networkType);
+            } catch (NullPointerException npe) {
+                loge("setNotificaitionVisible: cancel notificationManager npe=" + npe);
+                npe.printStackTrace();
+            }
+        }
+        mIsNotificationVisible = visible;
+    }
+
+    /** Location to an updatable file listing carrier provisioning urls.
+     *  An example:
+     *
+     * <?xml version="1.0" encoding="utf-8"?>
+     *  <provisioningUrls>
+     *   <provisioningUrl mcc="310" mnc="4">http://myserver.com/foo?mdn=%3$s&amp;iccid=%1$s&amp;imei=%2$s</provisioningUrl>
+     *   <redirectedUrl mcc="310" mnc="4">http://www.google.com</redirectedUrl>
+     *  </provisioningUrls>
+     */
+    private static final String PROVISIONING_URL_PATH =
+            "/data/misc/radio/provisioning_urls.xml";
+    private final File mProvisioningUrlFile = new File(PROVISIONING_URL_PATH);
+
+    /** XML tag for root element. */
+    private static final String TAG_PROVISIONING_URLS = "provisioningUrls";
+    /** XML tag for individual url */
+    private static final String TAG_PROVISIONING_URL = "provisioningUrl";
+    /** XML tag for redirected url */
+    private static final String TAG_REDIRECTED_URL = "redirectedUrl";
+    /** XML attribute for mcc */
+    private static final String ATTR_MCC = "mcc";
+    /** XML attribute for mnc */
+    private static final String ATTR_MNC = "mnc";
+
+    private static final int REDIRECTED_PROVISIONING = 1;
+    private static final int PROVISIONING = 2;
+
+    private String getProvisioningUrlBaseFromFile(int type) {
+        FileReader fileReader = null;
+        XmlPullParser parser = null;
+        Configuration config = mContext.getResources().getConfiguration();
+        String tagType;
+
+        switch (type) {
+            case PROVISIONING:
+                tagType = TAG_PROVISIONING_URL;
+                break;
+            case REDIRECTED_PROVISIONING:
+                tagType = TAG_REDIRECTED_URL;
+                break;
+            default:
+                throw new RuntimeException("getProvisioningUrlBaseFromFile: Unexpected parameter " +
+                        type);
+        }
+
+        try {
+            fileReader = new FileReader(mProvisioningUrlFile);
+            parser = Xml.newPullParser();
+            parser.setInput(fileReader);
+            XmlUtils.beginDocument(parser, TAG_PROVISIONING_URLS);
+
+            while (true) {
+                XmlUtils.nextElement(parser);
+
+                String element = parser.getName();
+                if (element == null) break;
+
+                if (element.equals(tagType)) {
+                    String mcc = parser.getAttributeValue(null, ATTR_MCC);
+                    try {
+                        if (mcc != null && Integer.parseInt(mcc) == config.mcc) {
+                            String mnc = parser.getAttributeValue(null, ATTR_MNC);
+                            if (mnc != null && Integer.parseInt(mnc) == config.mnc) {
+                                parser.next();
+                                if (parser.getEventType() == XmlPullParser.TEXT) {
+                                    return parser.getText();
+                                }
+                            }
+                        }
+                    } catch (NumberFormatException e) {
+                        loge("NumberFormatException in getProvisioningUrlBaseFromFile: " + e);
+                    }
+                }
+            }
+            return null;
+        } catch (FileNotFoundException e) {
+            loge("Carrier Provisioning Urls file not found");
+        } catch (XmlPullParserException e) {
+            loge("Xml parser exception reading Carrier Provisioning Urls file: " + e);
+        } catch (IOException e) {
+            loge("I/O exception reading Carrier Provisioning Urls file: " + e);
+        } finally {
+            if (fileReader != null) {
+                try {
+                    fileReader.close();
+                } catch (IOException e) {}
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public String getMobileRedirectedProvisioningUrl() {
+        enforceConnectivityInternalPermission();
+        String url = getProvisioningUrlBaseFromFile(REDIRECTED_PROVISIONING);
+        if (TextUtils.isEmpty(url)) {
+            url = mContext.getResources().getString(R.string.mobile_redirected_provisioning_url);
+        }
+        return url;
+    }
+
+    @Override
+    public String getMobileProvisioningUrl() {
+        enforceConnectivityInternalPermission();
+        String url = getProvisioningUrlBaseFromFile(PROVISIONING);
+        if (TextUtils.isEmpty(url)) {
+            url = mContext.getResources().getString(R.string.mobile_provisioning_url);
+            log("getMobileProvisioningUrl: mobile_provisioining_url from resource =" + url);
+        } else {
+            log("getMobileProvisioningUrl: mobile_provisioning_url from File =" + url);
+        }
+        // populate the iccid, imei and phone number in the provisioning url.
+        if (!TextUtils.isEmpty(url)) {
+            String phoneNumber = mTelephonyManager.getLine1Number();
+            if (TextUtils.isEmpty(phoneNumber)) {
+                phoneNumber = "0000000000";
+            }
+            url = String.format(url,
+                    mTelephonyManager.getSimSerialNumber() /* ICCID */,
+                    mTelephonyManager.getDeviceId() /* IMEI */,
+                    phoneNumber /* Phone numer */);
+        }
+
+        return url;
+    }
+
+    @Override
+    public void setProvisioningNotificationVisible(boolean visible, int networkType,
+            String extraInfo, String url) {
+        enforceConnectivityInternalPermission();
+        setProvNotificationVisible(visible, networkType, extraInfo, url);
+    }
+
+    @Override
+    public void setAirplaneMode(boolean enable) {
+        enforceConnectivityInternalPermission();
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            final ContentResolver cr = mContext.getContentResolver();
+            Settings.Global.putInt(cr, Settings.Global.AIRPLANE_MODE_ON, enable ? 1 : 0);
+            Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+            intent.putExtra("state", enable);
+            mContext.sendBroadcast(intent);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private void onUserStart(int userId) {
+        synchronized(mVpns) {
+            Vpn userVpn = mVpns.get(userId);
+            if (userVpn != null) {
+                loge("Starting user already has a VPN");
+                return;
+            }
+            userVpn = new Vpn(mContext, mVpnCallback, mNetd, this, userId);
+            mVpns.put(userId, userVpn);
+            userVpn.startMonitoring(mContext, mTrackerHandler);
+        }
+    }
+
+    private void onUserStop(int userId) {
+        synchronized(mVpns) {
+            Vpn userVpn = mVpns.get(userId);
+            if (userVpn == null) {
+                loge("Stopping user has no VPN");
+                return;
+            }
+            mVpns.delete(userId);
+        }
+    }
+
+    private BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+            if (userId == UserHandle.USER_NULL) return;
+
+            if (Intent.ACTION_USER_STARTING.equals(action)) {
+                onUserStart(userId);
+            } else if (Intent.ACTION_USER_STOPPING.equals(action)) {
+                onUserStop(userId);
+            }
+        }
+    };
+
+    @Override
+    public LinkQualityInfo getLinkQualityInfo(int networkType) {
+        enforceAccessPermission();
+        if (isNetworkTypeValid(networkType) && mNetTrackers[networkType] != null) {
+            return mNetTrackers[networkType].getLinkQualityInfo();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public LinkQualityInfo getActiveLinkQualityInfo() {
+        enforceAccessPermission();
+        if (isNetworkTypeValid(mActiveDefaultNetwork) &&
+                mNetTrackers[mActiveDefaultNetwork] != null) {
+            return mNetTrackers[mActiveDefaultNetwork].getLinkQualityInfo();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public LinkQualityInfo[] getAllLinkQualityInfo() {
+        enforceAccessPermission();
+        final ArrayList<LinkQualityInfo> result = Lists.newArrayList();
+        for (NetworkStateTracker tracker : mNetTrackers) {
+            if (tracker != null) {
+                LinkQualityInfo li = tracker.getLinkQualityInfo();
+                if (li != null) {
+                    result.add(li);
+                }
+            }
+        }
+
+        return result.toArray(new LinkQualityInfo[result.size()]);
+    }
+
+    /* Infrastructure for network sampling */
+
+    private void handleNetworkSamplingTimeout() {
+
+        log("Sampling interval elapsed, updating statistics ..");
+
+        // initialize list of interfaces ..
+        Map<String, SamplingDataTracker.SamplingSnapshot> mapIfaceToSample =
+                new HashMap<String, SamplingDataTracker.SamplingSnapshot>();
+        for (NetworkStateTracker tracker : mNetTrackers) {
+            if (tracker != null) {
+                String ifaceName = tracker.getNetworkInterfaceName();
+                if (ifaceName != null) {
+                    mapIfaceToSample.put(ifaceName, null);
+                }
+            }
+        }
+
+        // Read samples for all interfaces
+        SamplingDataTracker.getSamplingSnapshots(mapIfaceToSample);
+
+        // process samples for all networks
+        for (NetworkStateTracker tracker : mNetTrackers) {
+            if (tracker != null) {
+                String ifaceName = tracker.getNetworkInterfaceName();
+                SamplingDataTracker.SamplingSnapshot ss = mapIfaceToSample.get(ifaceName);
+                if (ss != null) {
+                    // end the previous sampling cycle
+                    tracker.stopSampling(ss);
+                    // start a new sampling cycle ..
+                    tracker.startSampling(ss);
+                }
+            }
+        }
+
+        log("Done.");
+
+        int samplingIntervalInSeconds = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.CONNECTIVITY_SAMPLING_INTERVAL_IN_SECONDS,
+                DEFAULT_SAMPLING_INTERVAL_IN_SECONDS);
+
+        if (DBG) log("Setting timer for " + String.valueOf(samplingIntervalInSeconds) + "seconds");
+
+        setAlarm(samplingIntervalInSeconds * 1000, mSampleIntervalElapsedIntent);
+    }
+
+    void setAlarm(int timeoutInMilliseconds, PendingIntent intent) {
+        long wakeupTime = SystemClock.elapsedRealtime() + timeoutInMilliseconds;
+        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, wakeupTime, intent);
+    }
+
+    private final HashMap<Messenger, NetworkFactoryInfo> mNetworkFactoryInfos =
+            new HashMap<Messenger, NetworkFactoryInfo>();
+    private final HashMap<NetworkRequest, NetworkRequestInfo> mNetworkRequests =
+            new HashMap<NetworkRequest, NetworkRequestInfo>();
+
+    private static class NetworkFactoryInfo {
+        public final String name;
+        public final Messenger messenger;
+        public final AsyncChannel asyncChannel;
+
+        public NetworkFactoryInfo(String name, Messenger messenger, AsyncChannel asyncChannel) {
+            this.name = name;
+            this.messenger = messenger;
+            this.asyncChannel = asyncChannel;
+        }
+    }
+
+    private class NetworkRequestInfo implements IBinder.DeathRecipient {
+        static final boolean REQUEST = true;
+        static final boolean LISTEN = false;
+
+        final NetworkRequest request;
+        IBinder mBinder;
+        final int mPid;
+        final int mUid;
+        final Messenger messenger;
+        final boolean isRequest;
+
+        NetworkRequestInfo(Messenger m, NetworkRequest r, IBinder binder, boolean isRequest) {
+            super();
+            messenger = m;
+            request = r;
+            mBinder = binder;
+            mPid = getCallingPid();
+            mUid = getCallingUid();
+            this.isRequest = isRequest;
+
+            try {
+                mBinder.linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                binderDied();
+            }
+        }
+
+        void unlinkDeathRecipient() {
+            mBinder.unlinkToDeath(this, 0);
+        }
+
+        public void binderDied() {
+            log("ConnectivityService NetworkRequestInfo binderDied(" +
+                    request + ", " + mBinder + ")");
+            releaseNetworkRequest(request);
+        }
+
+        public String toString() {
+            return (isRequest ? "Request" : "Listen") + " from uid/pid:" + mUid + "/" +
+                    mPid + " for " + request;
+        }
+    }
+
+    @Override
+    public NetworkRequest requestNetwork(NetworkCapabilities networkCapabilities,
+            Messenger messenger, int timeoutSec, IBinder binder, int legacyType) {
+        if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                == false) {
+            enforceConnectivityInternalPermission();
+        } else {
+            enforceChangePermission();
+        }
+
+        if (timeoutSec < 0 || timeoutSec > ConnectivityManager.MAX_NETWORK_REQUEST_TIMEOUT_SEC) {
+            throw new IllegalArgumentException("Bad timeout specified");
+        }
+        NetworkRequest networkRequest = new NetworkRequest(new NetworkCapabilities(
+                networkCapabilities), legacyType, nextNetworkRequestId());
+        if (DBG) log("requestNetwork for " + networkRequest);
+        NetworkRequestInfo nri = new NetworkRequestInfo(messenger, networkRequest, binder,
+                NetworkRequestInfo.REQUEST);
+
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST, nri));
+        if (timeoutSec > 0) {
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_TIMEOUT_NETWORK_REQUEST,
+                    nri), timeoutSec * 1000);
+        }
+        return networkRequest;
+    }
+
+    @Override
+    public NetworkRequest pendingRequestForNetwork(NetworkCapabilities networkCapabilities,
+            PendingIntent operation) {
+        // TODO
+        return null;
+    }
+
+    @Override
+    public NetworkRequest listenForNetwork(NetworkCapabilities networkCapabilities,
+            Messenger messenger, IBinder binder) {
+        enforceAccessPermission();
+
+        NetworkRequest networkRequest = new NetworkRequest(new NetworkCapabilities(
+                networkCapabilities), TYPE_NONE, nextNetworkRequestId());
+        if (DBG) log("listenForNetwork for " + networkRequest);
+        NetworkRequestInfo nri = new NetworkRequestInfo(messenger, networkRequest, binder,
+                NetworkRequestInfo.LISTEN);
+
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri));
+        return networkRequest;
+    }
+
+    @Override
+    public void pendingListenForNetwork(NetworkCapabilities networkCapabilities,
+            PendingIntent operation) {
+    }
+
+    @Override
+    public void releaseNetworkRequest(NetworkRequest networkRequest) {
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_RELEASE_NETWORK_REQUEST,
+                networkRequest));
+    }
+
+    @Override
+    public void registerNetworkFactory(Messenger messenger, String name) {
+        enforceConnectivityInternalPermission();
+        NetworkFactoryInfo nfi = new NetworkFactoryInfo(name, messenger, new AsyncChannel());
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_FACTORY, nfi));
+    }
+
+    private void handleRegisterNetworkFactory(NetworkFactoryInfo nfi) {
+        if (VDBG) log("Got NetworkFactory Messenger for " + nfi.name);
+        mNetworkFactoryInfos.put(nfi.messenger, nfi);
+        nfi.asyncChannel.connect(mContext, mTrackerHandler, nfi.messenger);
+    }
+
+    @Override
+    public void unregisterNetworkFactory(Messenger messenger) {
+        enforceConnectivityInternalPermission();
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_UNREGISTER_NETWORK_FACTORY, messenger));
+    }
+
+    private void handleUnregisterNetworkFactory(Messenger messenger) {
+        NetworkFactoryInfo nfi = mNetworkFactoryInfos.remove(messenger);
+        if (nfi == null) {
+            if (VDBG) log("Failed to find Messenger in unregisterNetworkFactory");
+            return;
+        }
+        if (VDBG) log("unregisterNetworkFactory for " + nfi.name);
+    }
+
+    /**
+     * NetworkAgentInfo supporting a request by requestId.
+     * These have already been vetted (their Capabilities satisfy the request)
+     * and the are the highest scored network available.
+     * the are keyed off the Requests requestId.
+     */
+    private final SparseArray<NetworkAgentInfo> mNetworkForRequestId =
+            new SparseArray<NetworkAgentInfo>();
+
+    private final SparseArray<NetworkAgentInfo> mNetworkForNetId =
+            new SparseArray<NetworkAgentInfo>();
+
+    // NetworkAgentInfo keyed off its connecting messenger
+    // TODO - eval if we can reduce the number of lists/hashmaps/sparsearrays
+    private final HashMap<Messenger, NetworkAgentInfo> mNetworkAgentInfos =
+            new HashMap<Messenger, NetworkAgentInfo>();
+
+    private final NetworkRequest mDefaultRequest;
+
+    public void registerNetworkAgent(Messenger messenger, NetworkInfo networkInfo,
+            LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
+            int currentScore) {
+        enforceConnectivityInternalPermission();
+
+        NetworkAgentInfo nai = new NetworkAgentInfo(messenger, new AsyncChannel(), nextNetId(),
+            new NetworkInfo(networkInfo), new LinkProperties(linkProperties),
+            new NetworkCapabilities(networkCapabilities), currentScore, mContext, mTrackerHandler);
+        if (VDBG) log("registerNetworkAgent " + nai);
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT, nai));
+    }
+
+    private void handleRegisterNetworkAgent(NetworkAgentInfo na) {
+        if (VDBG) log("Got NetworkAgent Messenger");
+        mNetworkAgentInfos.put(na.messenger, na);
+        mNetworkForNetId.put(na.network.netId, na);
+        na.asyncChannel.connect(mContext, mTrackerHandler, na.messenger);
+        NetworkInfo networkInfo = na.networkInfo;
+        na.networkInfo = null;
+        updateNetworkInfo(na, networkInfo);
+    }
+
+    private void updateLinkProperties(NetworkAgentInfo networkAgent, LinkProperties oldLp) {
+        LinkProperties newLp = networkAgent.linkProperties;
+        int netId = networkAgent.network.netId;
+
+        updateInterfaces(newLp, oldLp, netId);
+        updateMtu(newLp, oldLp);
+        // TODO - figure out what to do for clat
+//        for (LinkProperties lp : newLp.getStackedLinks()) {
+//            updateMtu(lp, null);
+//        }
+        updateRoutes(newLp, oldLp, netId);
+        updateDnses(newLp, oldLp, netId);
+        updateClat(newLp, oldLp, networkAgent);
+    }
+
+    private void updateClat(LinkProperties newLp, LinkProperties oldLp, NetworkAgentInfo na) {
+        // Update 464xlat state.
+        if (mClat.requiresClat(na)) {
+
+            // If the connection was previously using clat, but is not using it now, stop the clat
+            // daemon. Normally, this happens automatically when the connection disconnects, but if
+            // the disconnect is not reported, or if the connection's LinkProperties changed for
+            // some other reason (e.g., handoff changes the IP addresses on the link), it would
+            // still be running. If it's not running, then stopping it is a no-op.
+            if (Nat464Xlat.isRunningClat(oldLp) && !Nat464Xlat.isRunningClat(newLp)) {
+                mClat.stopClat();
+            }
+            // If the link requires clat to be running, then start the daemon now.
+            if (na.networkInfo.isConnected()) {
+                mClat.startClat(na);
+            } else {
+                mClat.stopClat();
+            }
+        }
+    }
+
+    private void updateInterfaces(LinkProperties newLp, LinkProperties oldLp, int netId) {
+        CompareResult<String> interfaceDiff = new CompareResult<String>();
+        if (oldLp != null) {
+            interfaceDiff = oldLp.compareAllInterfaceNames(newLp);
+        } else if (newLp != null) {
+            interfaceDiff.added = newLp.getAllInterfaceNames();
+        }
+        for (String iface : interfaceDiff.added) {
+            try {
+                mNetd.addInterfaceToNetwork(iface, netId);
+            } catch (Exception e) {
+                loge("Exception adding interface: " + e);
+            }
+        }
+        for (String iface : interfaceDiff.removed) {
+            try {
+                mNetd.removeInterfaceFromNetwork(iface, netId);
+            } catch (Exception e) {
+                loge("Exception removing interface: " + e);
+            }
+        }
+    }
+
+    private void updateRoutes(LinkProperties newLp, LinkProperties oldLp, int netId) {
+        CompareResult<RouteInfo> routeDiff = new CompareResult<RouteInfo>();
+        if (oldLp != null) {
+            routeDiff = oldLp.compareAllRoutes(newLp);
+        } else if (newLp != null) {
+            routeDiff.added = newLp.getAllRoutes();
+        }
+
+        // add routes before removing old in case it helps with continuous connectivity
+
+        // do this twice, adding non-nexthop routes first, then routes they are dependent on
+        for (RouteInfo route : routeDiff.added) {
+            if (route.hasGateway()) continue;
+            try {
+                mNetd.addRoute(netId, route);
+            } catch (Exception e) {
+                loge("Exception in addRoute for non-gateway: " + e);
+            }
+        }
+        for (RouteInfo route : routeDiff.added) {
+            if (route.hasGateway() == false) continue;
+            try {
+                mNetd.addRoute(netId, route);
+            } catch (Exception e) {
+                loge("Exception in addRoute for gateway: " + e);
+            }
+        }
+
+        for (RouteInfo route : routeDiff.removed) {
+            try {
+                mNetd.removeRoute(netId, route);
+            } catch (Exception e) {
+                loge("Exception in removeRoute: " + e);
+            }
+        }
+    }
+    private void updateDnses(LinkProperties newLp, LinkProperties oldLp, int netId) {
+        if (oldLp == null || (newLp.isIdenticalDnses(oldLp) == false)) {
+            Collection<InetAddress> dnses = newLp.getDnsServers();
+            if (dnses.size() == 0 && mDefaultDns != null) {
+                dnses = new ArrayList();
+                dnses.add(mDefaultDns);
+                if (DBG) {
+                    loge("no dns provided for netId " + netId + ", so using defaults");
+                }
+            }
+            try {
+                mNetd.setDnsServersForNetwork(netId, NetworkUtils.makeStrings(dnses),
+                    newLp.getDomains());
+            } catch (Exception e) {
+                loge("Exception in setDnsServersForNetwork: " + e);
+            }
+            NetworkAgentInfo defaultNai = mNetworkForRequestId.get(mDefaultRequest.requestId);
+            if (defaultNai != null && defaultNai.network.netId == netId) {
+                setDefaultDnsSystemProperties(dnses);
+            }
+        }
+    }
+
+    private void setDefaultDnsSystemProperties(Collection<InetAddress> dnses) {
+        int last = 0;
+        for (InetAddress dns : dnses) {
+            ++last;
+            String key = "net.dns" + last;
+            String value = dns.getHostAddress();
+            SystemProperties.set(key, value);
+        }
+        for (int i = last + 1; i <= mNumDnsEntries; ++i) {
+            String key = "net.dns" + i;
+            SystemProperties.set(key, "");
+        }
+        mNumDnsEntries = last;
+    }
+
+
+    private void updateCapabilities(NetworkAgentInfo networkAgent,
+            NetworkCapabilities networkCapabilities) {
+        // TODO - what else here?  Verify still satisfies everybody?
+        // Check if satisfies somebody new?  call callbacks?
+        networkAgent.networkCapabilities = networkCapabilities;
+    }
+
+    private void sendUpdatedScoreToFactories(NetworkRequest networkRequest, int score) {
+        if (VDBG) log("sending new Min Network Score(" + score + "): " + networkRequest.toString());
+        for (NetworkFactoryInfo nfi : mNetworkFactoryInfos.values()) {
+            nfi.asyncChannel.sendMessage(android.net.NetworkFactory.CMD_REQUEST_NETWORK, score, 0,
+                    networkRequest);
+        }
+    }
+
+    private void callCallbackForRequest(NetworkRequestInfo nri,
+            NetworkAgentInfo networkAgent, int notificationType) {
+        if (nri.messenger == null) return;  // Default request has no msgr
+        Object o;
+        int a1 = 0;
+        int a2 = 0;
+        switch (notificationType) {
+            case ConnectivityManager.CALLBACK_LOSING:
+                a1 = 30; // TODO - read this from NetworkMonitor
+                // fall through
+            case ConnectivityManager.CALLBACK_PRECHECK:
+            case ConnectivityManager.CALLBACK_AVAILABLE:
+            case ConnectivityManager.CALLBACK_LOST:
+            case ConnectivityManager.CALLBACK_CAP_CHANGED:
+            case ConnectivityManager.CALLBACK_IP_CHANGED: {
+                o = new NetworkRequest(nri.request);
+                a2 = networkAgent.network.netId;
+                break;
+            }
+            case ConnectivityManager.CALLBACK_UNAVAIL:
+            case ConnectivityManager.CALLBACK_RELEASED: {
+                o = new NetworkRequest(nri.request);
+                break;
+            }
+            default: {
+                loge("Unknown notificationType " + notificationType);
+                return;
+            }
+        }
+        Message msg = Message.obtain();
+        msg.arg1 = a1;
+        msg.arg2 = a2;
+        msg.obj = o;
+        msg.what = notificationType;
+        try {
+            if (VDBG) log("sending notification " + notificationType + " for " + nri.request);
+            nri.messenger.send(msg);
+        } catch (RemoteException e) {
+            // may occur naturally in the race of binder death.
+            loge("RemoteException caught trying to send a callback msg for " + nri.request);
+        }
+    }
+
+    private void handleLingerComplete(NetworkAgentInfo oldNetwork) {
+        if (oldNetwork == null) {
+            loge("Unknown NetworkAgentInfo in handleLingerComplete");
+            return;
+        }
+        if (DBG) log("handleLingerComplete for " + oldNetwork.name());
+        if (DBG) {
+            if (oldNetwork.networkRequests.size() != 0) {
+                loge("Dead network still had " + oldNetwork.networkRequests.size() + " requests");
+            }
+        }
+        oldNetwork.asyncChannel.disconnect();
+    }
+
+    private void handleConnectionValidated(NetworkAgentInfo newNetwork) {
+        if (newNetwork == null) {
+            loge("Unknown NetworkAgentInfo in handleConnectionValidated");
+            return;
+        }
+        boolean keep = false;
+        boolean isNewDefault = false;
+        if (DBG) log("handleConnectionValidated for "+newNetwork.name());
+        // check if any NetworkRequest wants this NetworkAgent
+        ArrayList<NetworkAgentInfo> affectedNetworks = new ArrayList<NetworkAgentInfo>();
+        if (VDBG) log(" new Network has: " + newNetwork.networkCapabilities);
+        for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+            NetworkAgentInfo currentNetwork = mNetworkForRequestId.get(nri.request.requestId);
+            if (newNetwork == currentNetwork) {
+                if (VDBG) log("Network " + newNetwork.name() + " was already satisfying" +
+                              " request " + nri.request.requestId + ". No change.");
+                keep = true;
+                continue;
+            }
+
+            // check if it satisfies the NetworkCapabilities
+            if (VDBG) log("  checking if request is satisfied: " + nri.request);
+            if (nri.request.networkCapabilities.satisfiedByNetworkCapabilities(
+                    newNetwork.networkCapabilities)) {
+                // next check if it's better than any current network we're using for
+                // this request
+                if (VDBG) {
+                    log("currentScore = " +
+                            (currentNetwork != null ? currentNetwork.currentScore : 0) +
+                            ", newScore = " + newNetwork.currentScore);
+                }
+                if (currentNetwork == null ||
+                        currentNetwork.currentScore < newNetwork.currentScore) {
+                    if (currentNetwork != null) {
+                        if (VDBG) log("   accepting network in place of " + currentNetwork.name());
+                        currentNetwork.networkRequests.remove(nri.request.requestId);
+                        currentNetwork.networkLingered.add(nri.request);
+                        affectedNetworks.add(currentNetwork);
+                    } else {
+                        if (VDBG) log("   accepting network in place of null");
+                    }
+                    mNetworkForRequestId.put(nri.request.requestId, newNetwork);
+                    newNetwork.addRequest(nri.request);
+                    int legacyType = nri.request.legacyType;
+                    if (legacyType != TYPE_NONE) {
+                        mLegacyTypeTracker.add(legacyType, newNetwork);
+                    }
+                    keep = true;
+                    // TODO - this could get expensive if we have alot of requests for this
+                    // network.  Think about if there is a way to reduce this.  Push
+                    // netid->request mapping to each factory?
+                    sendUpdatedScoreToFactories(nri.request, newNetwork.currentScore);
+                    if (mDefaultRequest.requestId == nri.request.requestId) {
+                        isNewDefault = true;
+                        updateActiveDefaultNetwork(newNetwork);
+                        if (newNetwork.linkProperties != null) {
+                            setDefaultDnsSystemProperties(
+                                    newNetwork.linkProperties.getDnsServers());
+                        } else {
+                            setDefaultDnsSystemProperties(new ArrayList<InetAddress>());
+                        }
+                        mLegacyTypeTracker.add(newNetwork.networkInfo.getType(), newNetwork);
+                    }
+                }
+            }
+        }
+        for (NetworkAgentInfo nai : affectedNetworks) {
+            boolean teardown = true;
+            for (int i = 0; i < nai.networkRequests.size(); i++) {
+                NetworkRequest nr = nai.networkRequests.valueAt(i);
+                try {
+                if (mNetworkRequests.get(nr).isRequest) {
+                    teardown = false;
+                }
+                } catch (Exception e) {
+                    loge("Request " + nr + " not found in mNetworkRequests.");
+                    loge("  it came from request list  of " + nai.name());
+                }
+            }
+            if (teardown) {
+                nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_LINGER);
+                notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING);
+            } else {
+                // not going to linger, so kill the list of linger networks..  only
+                // notify them of linger if it happens as the result of gaining another,
+                // but if they transition and old network stays up, don't tell them of linger
+                // or very delayed loss
+                nai.networkLingered.clear();
+                if (VDBG) log("Lingered for " + nai.name() + " cleared");
+            }
+        }
+        if (keep) {
+            if (isNewDefault) {
+                if (VDBG) log("Switching to new default network: " + newNetwork);
+                setupDataActivityTracking(newNetwork);
+                try {
+                    mNetd.setDefaultNetId(newNetwork.network.netId);
+                } catch (Exception e) {
+                    loge("Exception setting default network :" + e);
+                }
+                if (newNetwork.equals(mNetworkForRequestId.get(mDefaultRequest.requestId))) {
+                    handleApplyDefaultProxy(newNetwork.linkProperties.getHttpProxy());
+                }
+                synchronized (ConnectivityService.this) {
+                    // have a new default network, release the transition wakelock in
+                    // a second if it's held.  The second pause is to allow apps
+                    // to reconnect over the new network
+                    if (mNetTransitionWakeLock.isHeld()) {
+                        mHandler.sendMessageDelayed(mHandler.obtainMessage(
+                                EVENT_CLEAR_NET_TRANSITION_WAKELOCK,
+                                mNetTransitionWakeLockSerialNumber, 0),
+                                1000);
+                    }
+                }
+
+                // this will cause us to come up initially as unconnected and switching
+                // to connected after our normal pause unless somebody reports us as
+                // really disconnected
+                mDefaultInetConditionPublished = 0;
+                mDefaultConnectionSequence++;
+                mInetConditionChangeInFlight = false;
+                // TODO - read the tcp buffer size config string from somewhere
+                // updateNetworkSettings();
+            }
+            // notify battery stats service about this network
+            try {
+                BatteryStatsService.getService().noteNetworkInterfaceType(
+                        newNetwork.linkProperties.getInterfaceName(),
+                        newNetwork.networkInfo.getType());
+            } catch (RemoteException e) { }
+            notifyNetworkCallbacks(newNetwork, ConnectivityManager.CALLBACK_AVAILABLE);
+        } else {
+            if (DBG && newNetwork.networkRequests.size() != 0) {
+                loge("tearing down network with live requests:");
+                for (int i=0; i < newNetwork.networkRequests.size(); i++) {
+                    loge("  " + newNetwork.networkRequests.valueAt(i));
+                }
+            }
+            if (VDBG) log("Validated network turns out to be unwanted.  Tear it down.");
+            newNetwork.asyncChannel.disconnect();
+        }
+    }
+
+
+    private void updateNetworkInfo(NetworkAgentInfo networkAgent, NetworkInfo newInfo) {
+        NetworkInfo.State state = newInfo.getState();
+        NetworkInfo oldInfo = networkAgent.networkInfo;
+        networkAgent.networkInfo = newInfo;
+
+        if (oldInfo != null && oldInfo.getState() == state) {
+            if (VDBG) log("ignoring duplicate network state non-change");
+            return;
+        }
+        if (DBG) {
+            log(networkAgent.name() + " EVENT_NETWORK_INFO_CHANGED, going from " +
+                    (oldInfo == null ? "null" : oldInfo.getState()) +
+                    " to " + state);
+        }
+
+        if (state == NetworkInfo.State.CONNECTED) {
+            try {
+                // This is likely caused by the fact that this network already
+                // exists. An example is when a network goes from CONNECTED to
+                // CONNECTING and back (like wifi on DHCP renew).
+                // TODO: keep track of which networks we've created, or ask netd
+                // to tell us whether we've already created this network or not.
+                mNetd.createNetwork(networkAgent.network.netId);
+            } catch (Exception e) {
+                loge("Error creating network " + networkAgent.network.netId + ": "
+                        + e.getMessage());
+                return;
+            }
+
+            updateLinkProperties(networkAgent, null);
+            notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_PRECHECK);
+            networkAgent.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_CONNECTED);
+        } else if (state == NetworkInfo.State.DISCONNECTED ||
+                state == NetworkInfo.State.SUSPENDED) {
+            networkAgent.asyncChannel.disconnect();
+        }
+    }
+
+    private void updateNetworkScore(NetworkAgentInfo nai, int score) {
+        if (DBG) log("updateNetworkScore for " + nai.name() + " to " + score);
+
+        nai.currentScore = score;
+
+        // TODO - This will not do the right thing if this network is lowering
+        // its score and has requests that can be served by other
+        // currently-active networks, or if the network is increasing its
+        // score and other networks have requests that can be better served
+        // by this network.
+        //
+        // Really we want to see if any of our requests migrate to other
+        // active/lingered networks and if any other requests migrate to us (depending
+        // on increasing/decreasing currentScore.  That's a bit of work and probably our
+        // score checking/network allocation code needs to be modularized so we can understand
+        // (see handleConnectionValided for an example).
+        //
+        // As a first order approx, lets just advertise the new score to factories.  If
+        // somebody can beat it they will nominate a network and our normal net replacement
+        // code will fire.
+        for (int i = 0; i < nai.networkRequests.size(); i++) {
+            NetworkRequest nr = nai.networkRequests.valueAt(i);
+            sendUpdatedScoreToFactories(nr, score);
+        }
+    }
+
+    // notify only this one new request of the current state
+    protected void notifyNetworkCallback(NetworkAgentInfo nai, NetworkRequestInfo nri) {
+        int notifyType = ConnectivityManager.CALLBACK_AVAILABLE;
+        // TODO - read state from monitor to decide what to send.
+//        if (nai.networkMonitor.isLingering()) {
+//            notifyType = NetworkCallbacks.LOSING;
+//        } else if (nai.networkMonitor.isEvaluating()) {
+//            notifyType = NetworkCallbacks.callCallbackForRequest(request, nai, notifyType);
+//        }
+        callCallbackForRequest(nri, nai, notifyType);
+    }
+
+    private void sendLegacyNetworkBroadcast(NetworkAgentInfo nai, boolean connected, int type) {
+        if (connected) {
+            NetworkInfo info = new NetworkInfo(nai.networkInfo);
+            info.setType(type);
+            sendConnectedBroadcastDelayed(info, getConnectivityChangeDelay());
+        } else {
+            NetworkInfo info = new NetworkInfo(nai.networkInfo);
+            info.setType(type);
+            Intent intent = new Intent(ConnectivityManager.CONNECTIVITY_ACTION);
+            intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, info);
+            intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType());
+            if (info.isFailover()) {
+                intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true);
+                nai.networkInfo.setFailover(false);
+            }
+            if (info.getReason() != null) {
+                intent.putExtra(ConnectivityManager.EXTRA_REASON, info.getReason());
+            }
+            if (info.getExtraInfo() != null) {
+                intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO, info.getExtraInfo());
+            }
+            NetworkAgentInfo newDefaultAgent = null;
+            if (nai.networkRequests.get(mDefaultRequest.requestId) != null) {
+                newDefaultAgent = mNetworkForRequestId.get(mDefaultRequest.requestId);
+                if (newDefaultAgent != null) {
+                    intent.putExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO,
+                            newDefaultAgent.networkInfo);
+                } else {
+                    intent.putExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, true);
+                }
+            }
+            intent.putExtra(ConnectivityManager.EXTRA_INET_CONDITION,
+                    mDefaultInetConditionPublished);
+            final Intent immediateIntent = new Intent(intent);
+            immediateIntent.setAction(CONNECTIVITY_ACTION_IMMEDIATE);
+            sendStickyBroadcast(immediateIntent);
+            sendStickyBroadcastDelayed(intent, getConnectivityChangeDelay());
+            if (newDefaultAgent != null) {
+                sendConnectedBroadcastDelayed(newDefaultAgent.networkInfo,
+                getConnectivityChangeDelay());
+            }
+        }
+    }
+
+    protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType) {
+        if (VDBG) log("notifyType " + notifyType + " for " + networkAgent.name());
+        for (int i = 0; i < networkAgent.networkRequests.size(); i++) {
+            NetworkRequest nr = networkAgent.networkRequests.valueAt(i);
+            NetworkRequestInfo nri = mNetworkRequests.get(nr);
+            if (VDBG) log(" sending notification for " + nr);
+            callCallbackForRequest(nri, networkAgent, notifyType);
+        }
+    }
+
+    private LinkProperties getLinkPropertiesForTypeInternal(int networkType) {
+        NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+        return (nai != null) ?
+                new LinkProperties(nai.linkProperties) :
+                new LinkProperties();
+    }
+
+    private NetworkInfo getNetworkInfoForType(int networkType) {
+        if (!mLegacyTypeTracker.isTypeSupported(networkType))
+            return null;
+
+        NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+        if (nai != null) {
+            NetworkInfo result = new NetworkInfo(nai.networkInfo);
+            result.setType(networkType);
+            return result;
+        } else {
+           return new NetworkInfo(networkType, 0, "Unknown", "");
+        }
+    }
+
+    private NetworkCapabilities getNetworkCapabilitiesForType(int networkType) {
+        NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+        return (nai != null) ?
+                new NetworkCapabilities(nai.networkCapabilities) :
+                new NetworkCapabilities();
+    }
+}
diff --git a/services/core/java/com/android/server/connectivity/Nat464Xlat.java b/services/core/java/com/android/server/connectivity/Nat464Xlat.java
new file mode 100644
index 0000000..096ab66
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/Nat464Xlat.java
@@ -0,0 +1,219 @@
+/*
+ * 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.connectivity;
+
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+
+import java.net.Inet4Address;
+
+import android.content.Context;
+import android.net.IConnectivityManager;
+import android.net.InterfaceConfiguration;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkUtils;
+import android.net.RouteInfo;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.INetworkManagementService;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.server.net.BaseNetworkObserver;
+
+/**
+ * @hide
+ *
+ * Class to manage a 464xlat CLAT daemon.
+ */
+public class Nat464Xlat extends BaseNetworkObserver {
+    private Context mContext;
+    private INetworkManagementService mNMService;
+    private IConnectivityManager mConnService;
+    // Whether we started clatd and expect it to be running.
+    private boolean mIsStarted;
+    // Whether the clatd interface exists (i.e., clatd is running).
+    private boolean mIsRunning;
+    // The LinkProperties of the clat interface.
+    private LinkProperties mLP;
+    // Current LinkProperties of the network.  Includes mLP as a stacked link when clat is active.
+    private LinkProperties mBaseLP;
+    // ConnectivityService Handler for LinkProperties updates.
+    private Handler mHandler;
+    // Marker to connote which network we're augmenting.
+    private Messenger mNetworkMessenger;
+
+    // This must match the interface name in clatd.conf.
+    private static final String CLAT_INTERFACE_NAME = "clat4";
+
+    private static final String TAG = "Nat464Xlat";
+
+    public Nat464Xlat(Context context, INetworkManagementService nmService,
+                      IConnectivityManager connService, Handler handler) {
+        mContext = context;
+        mNMService = nmService;
+        mConnService = connService;
+        mHandler = handler;
+
+        mIsStarted = false;
+        mIsRunning = false;
+        mLP = new LinkProperties();
+
+        // If this is a runtime restart, it's possible that clatd is already
+        // running, but we don't know about it. If so, stop it.
+        try {
+            if (mNMService.isClatdStarted()) {
+                mNMService.stopClatd();
+            }
+        } catch(RemoteException e) {}  // Well, we tried.
+    }
+
+    /**
+     * Determines whether a network requires clat.
+     * @param network the NetworkAgentInfo corresponding to the network.
+     * @return true if the network requires clat, false otherwise.
+     */
+    public boolean requiresClat(NetworkAgentInfo network) {
+        int netType = network.networkInfo.getType();
+        LinkProperties lp = network.linkProperties;
+        // Only support clat on mobile for now.
+        Slog.d(TAG, "requiresClat: netType=" + netType + ", hasIPv4Address=" +
+               lp.hasIPv4Address());
+        return netType == TYPE_MOBILE && !lp.hasIPv4Address();
+    }
+
+    public static boolean isRunningClat(LinkProperties lp) {
+      return lp != null && lp.getAllInterfaceNames().contains(CLAT_INTERFACE_NAME);
+    }
+
+    /**
+     * Starts the clat daemon.
+     * @param lp The link properties of the interface to start clatd on.
+     */
+    public void startClat(NetworkAgentInfo network) {
+        if (mNetworkMessenger != null && mNetworkMessenger != network.messenger) {
+            Slog.e(TAG, "startClat: too many networks requesting clat");
+            return;
+        }
+        mNetworkMessenger = network.messenger;
+        LinkProperties lp = network.linkProperties;
+        mBaseLP = new LinkProperties(lp);
+        if (mIsStarted) {
+            Slog.e(TAG, "startClat: already started");
+            return;
+        }
+        String iface = lp.getInterfaceName();
+        Slog.i(TAG, "Starting clatd on " + iface + ", lp=" + lp);
+        try {
+            mNMService.startClatd(iface);
+        } catch(RemoteException e) {
+            Slog.e(TAG, "Error starting clat daemon: " + e);
+        }
+        mIsStarted = true;
+    }
+
+    /**
+     * Stops the clat daemon.
+     */
+    public void stopClat() {
+        if (mIsStarted) {
+            Slog.i(TAG, "Stopping clatd");
+            try {
+                mNMService.stopClatd();
+            } catch(RemoteException e) {
+                Slog.e(TAG, "Error stopping clat daemon: " + e);
+            }
+            mIsStarted = false;
+            mIsRunning = false;
+            mNetworkMessenger = null;
+            mBaseLP = null;
+            mLP.clear();
+        } else {
+            Slog.e(TAG, "stopClat: already stopped");
+        }
+    }
+
+    public boolean isStarted() {
+        return mIsStarted;
+    }
+
+    public boolean isRunning() {
+        return mIsRunning;
+    }
+
+    private void updateConnectivityService() {
+        Message msg = mHandler.obtainMessage(
+            NetworkAgent.EVENT_NETWORK_PROPERTIES_CHANGED, mBaseLP);
+        msg.replyTo = mNetworkMessenger;
+        Slog.i(TAG, "sending message to ConnectivityService: " + msg);
+        msg.sendToTarget();
+    }
+
+    @Override
+    public void interfaceAdded(String iface) {
+        if (iface.equals(CLAT_INTERFACE_NAME)) {
+            Slog.i(TAG, "interface " + CLAT_INTERFACE_NAME +
+                   " added, mIsRunning = " + mIsRunning + " -> true");
+            mIsRunning = true;
+
+            // Create the LinkProperties for the clat interface by fetching the
+            // IPv4 address for the interface and adding an IPv4 default route,
+            // then stack the LinkProperties on top of the link it's running on.
+            // Although the clat interface is a point-to-point tunnel, we don't
+            // point the route directly at the interface because some apps don't
+            // understand routes without gateways (see, e.g., http://b/9597256
+            // http://b/9597516). Instead, set the next hop of the route to the
+            // clat IPv4 address itself (for those apps, it doesn't matter what
+            // the IP of the gateway is, only that there is one).
+            try {
+                InterfaceConfiguration config = mNMService.getInterfaceConfig(iface);
+                LinkAddress clatAddress = config.getLinkAddress();
+                mLP.clear();
+                mLP.setInterfaceName(iface);
+                RouteInfo ipv4Default = new RouteInfo(new LinkAddress(Inet4Address.ANY, 0),
+                                                      clatAddress.getAddress(), iface);
+                mLP.addRoute(ipv4Default);
+                mLP.addLinkAddress(clatAddress);
+                mBaseLP.addStackedLink(mLP);
+                Slog.i(TAG, "Adding stacked link. tracker LP: " + mBaseLP);
+                updateConnectivityService();
+            } catch(RemoteException e) {
+                Slog.e(TAG, "Error getting link properties: " + e);
+            }
+        }
+    }
+
+    @Override
+    public void interfaceRemoved(String iface) {
+        if (iface == CLAT_INTERFACE_NAME) {
+            if (mIsRunning) {
+                NetworkUtils.resetConnections(
+                    CLAT_INTERFACE_NAME,
+                    NetworkUtils.RESET_IPV4_ADDRESSES);
+                mBaseLP.removeStackedLink(mLP);
+                updateConnectivityService();
+            }
+            Slog.i(TAG, "interface " + CLAT_INTERFACE_NAME +
+                   " removed, mIsRunning = " + mIsRunning + " -> false");
+            mIsRunning = false;
+            mLP.clear();
+            Slog.i(TAG, "mLP = " + mLP);
+        }
+    }
+};
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
new file mode 100644
index 0000000..b03c247
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -0,0 +1,83 @@
+/*
+ * 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.connectivity;
+
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.Messenger;
+import android.util.SparseArray;
+
+import com.android.internal.util.AsyncChannel;
+import com.android.server.connectivity.NetworkMonitor;
+
+import java.util.ArrayList;
+
+/**
+ * A bag class used by ConnectivityService for holding a collection of most recent
+ * information published by a particular NetworkAgent as well as the
+ * AsyncChannel/messenger for reaching that NetworkAgent and lists of NetworkRequests
+ * interested in using it.
+ */
+public class NetworkAgentInfo {
+    public NetworkInfo networkInfo;
+    public final Network network;
+    public LinkProperties linkProperties;
+    public NetworkCapabilities networkCapabilities;
+    public int currentScore;
+    public final NetworkMonitor networkMonitor;
+
+    // The list of NetworkRequests being satisfied by this Network.
+    public final SparseArray<NetworkRequest> networkRequests = new SparseArray<NetworkRequest>();
+    public final ArrayList<NetworkRequest> networkLingered = new ArrayList<NetworkRequest>();
+
+    public final Messenger messenger;
+    public final AsyncChannel asyncChannel;
+
+    public NetworkAgentInfo(Messenger messenger, AsyncChannel ac, int netId, NetworkInfo info,
+            LinkProperties lp, NetworkCapabilities nc, int score, Context context,
+            Handler handler) {
+        this.messenger = messenger;
+        asyncChannel = ac;
+        network = new Network(netId);
+        networkInfo = info;
+        linkProperties = lp;
+        networkCapabilities = nc;
+        currentScore = score;
+        networkMonitor = new NetworkMonitor(context, handler, this);
+    }
+
+    public void addRequest(NetworkRequest networkRequest) {
+        networkRequests.put(networkRequest.requestId, networkRequest);
+    }
+
+    public String toString() {
+        return "NetworkAgentInfo{ ni{" + networkInfo + "}  network{" +
+                network + "}  lp{" +
+                linkProperties + "}  nc{" +
+                networkCapabilities + "}  Score{" + currentScore + "} }";
+    }
+
+    public String name() {
+        return "NetworkAgentInfo [" + networkInfo.getTypeName() + " (" +
+                networkInfo.getSubtypeName() + ")]";
+    }
+}
diff --git a/services/tests/servicestests/res/raw/netstats_uid_v4 b/services/tests/servicestests/res/raw/netstats_uid_v4
new file mode 100644
index 0000000..e75fc1c
--- /dev/null
+++ b/services/tests/servicestests/res/raw/netstats_uid_v4
Binary files differ
diff --git a/services/tests/servicestests/res/raw/netstats_v1 b/services/tests/servicestests/res/raw/netstats_v1
new file mode 100644
index 0000000..e80860a
--- /dev/null
+++ b/services/tests/servicestests/res/raw/netstats_v1
Binary files differ
diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
new file mode 100644
index 0000000..88aaafc
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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;
+
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivityManager.getNetworkTypeName;
+import static android.net.NetworkStateTracker.EVENT_STATE_CHANGED;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.INetworkPolicyManager;
+import android.net.INetworkStatsService;
+import android.net.LinkProperties;
+import android.net.NetworkConfig;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkStateTracker;
+import android.net.RouteInfo;
+import android.os.Handler;
+import android.os.INetworkManagementService;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.util.LogPrinter;
+
+import org.mockito.ArgumentCaptor;
+
+import java.net.InetAddress;
+import java.util.concurrent.Future;
+
+/**
+ * Tests for {@link ConnectivityService}.
+ */
+@LargeTest
+public class ConnectivityServiceTest extends AndroidTestCase {
+    private static final String TAG = "ConnectivityServiceTest";
+
+    private static final String MOBILE_IFACE = "rmnet3";
+    private static final String WIFI_IFACE = "wlan6";
+
+    private static final RouteInfo MOBILE_ROUTE_V4 = RouteInfo.makeHostRoute(parse("10.0.0.33"),
+                                                                             MOBILE_IFACE);
+    private static final RouteInfo MOBILE_ROUTE_V6 = RouteInfo.makeHostRoute(parse("fd00::33"),
+                                                                             MOBILE_IFACE);
+
+    private static final RouteInfo WIFI_ROUTE_V4 = RouteInfo.makeHostRoute(parse("192.168.0.66"),
+                                                                           parse("192.168.0.1"),
+                                                                           WIFI_IFACE);
+    private static final RouteInfo WIFI_ROUTE_V6 = RouteInfo.makeHostRoute(parse("fd00::66"),
+                                                                           parse("fd00::"),
+                                                                           WIFI_IFACE);
+
+    private INetworkManagementService mNetManager;
+    private INetworkStatsService mStatsService;
+    private INetworkPolicyManager mPolicyService;
+    private ConnectivityService.NetworkFactory mNetFactory;
+
+    private BroadcastInterceptingContext mServiceContext;
+    private ConnectivityService mService;
+
+    private MockNetwork mMobile;
+    private MockNetwork mWifi;
+
+    private Handler mTrackerHandler;
+
+    private static class MockNetwork {
+        public NetworkStateTracker tracker;
+        public NetworkInfo info;
+        public LinkProperties link;
+
+        public MockNetwork(int type) {
+            tracker = mock(NetworkStateTracker.class);
+            info = new NetworkInfo(type, -1, getNetworkTypeName(type), null);
+            link = new LinkProperties();
+        }
+
+        public void doReturnDefaults() {
+            // TODO: eventually CS should make defensive copies
+            doReturn(new NetworkInfo(info)).when(tracker).getNetworkInfo();
+            doReturn(new LinkProperties(link)).when(tracker).getLinkProperties();
+
+            // fallback to default TCP buffers
+            doReturn("").when(tracker).getTcpBufferSizesPropName();
+        }
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mServiceContext = new BroadcastInterceptingContext(getContext());
+
+        mNetManager = mock(INetworkManagementService.class);
+        mStatsService = mock(INetworkStatsService.class);
+        mPolicyService = mock(INetworkPolicyManager.class);
+        mNetFactory = mock(ConnectivityService.NetworkFactory.class);
+
+        mMobile = new MockNetwork(TYPE_MOBILE);
+        mWifi = new MockNetwork(TYPE_WIFI);
+
+        // omit most network trackers
+        doThrow(new IllegalArgumentException("Not supported in test environment"))
+                .when(mNetFactory).createTracker(anyInt(), isA(NetworkConfig.class));
+
+        doReturn(mMobile.tracker)
+                .when(mNetFactory).createTracker(eq(TYPE_MOBILE), isA(NetworkConfig.class));
+        doReturn(mWifi.tracker)
+                .when(mNetFactory).createTracker(eq(TYPE_WIFI), isA(NetworkConfig.class));
+
+        final ArgumentCaptor<Handler> trackerHandler = ArgumentCaptor.forClass(Handler.class);
+        doNothing().when(mMobile.tracker)
+                .startMonitoring(isA(Context.class), trackerHandler.capture());
+
+        mService = new ConnectivityService(
+                mServiceContext, mNetManager, mStatsService, mPolicyService, mNetFactory);
+        mService.systemReady();
+
+        mTrackerHandler = trackerHandler.getValue();
+        mTrackerHandler.getLooper().setMessageLogging(new LogPrinter(Log.INFO, TAG));
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    public void testMobileConnectedAddedRoutes() throws Exception {
+        Future<?> nextConnBroadcast;
+
+        // bring up mobile network
+        mMobile.info.setDetailedState(DetailedState.CONNECTED, null, null);
+        mMobile.link.setInterfaceName(MOBILE_IFACE);
+        mMobile.link.addRoute(MOBILE_ROUTE_V4);
+        mMobile.link.addRoute(MOBILE_ROUTE_V6);
+        mMobile.doReturnDefaults();
+
+        nextConnBroadcast = mServiceContext.nextBroadcastIntent(CONNECTIVITY_ACTION_IMMEDIATE);
+        mTrackerHandler.obtainMessage(EVENT_STATE_CHANGED, mMobile.info).sendToTarget();
+        nextConnBroadcast.get();
+
+        // verify that both routes were added and DNS was flushed
+        int mobileNetId = mMobile.tracker.getNetwork().netId;
+        verify(mNetManager).addRoute(eq(mobileNetId), eq(MOBILE_ROUTE_V4));
+        verify(mNetManager).addRoute(eq(mobileNetId), eq(MOBILE_ROUTE_V6));
+        verify(mNetManager).flushNetworkDnsCache(mobileNetId);
+
+    }
+
+    public void testMobileWifiHandoff() throws Exception {
+        Future<?> nextConnBroadcast;
+
+        // bring up mobile network
+        mMobile.info.setDetailedState(DetailedState.CONNECTED, null, null);
+        mMobile.link.setInterfaceName(MOBILE_IFACE);
+        mMobile.link.addRoute(MOBILE_ROUTE_V4);
+        mMobile.link.addRoute(MOBILE_ROUTE_V6);
+        mMobile.doReturnDefaults();
+
+        nextConnBroadcast = mServiceContext.nextBroadcastIntent(CONNECTIVITY_ACTION_IMMEDIATE);
+        mTrackerHandler.obtainMessage(EVENT_STATE_CHANGED, mMobile.info).sendToTarget();
+        nextConnBroadcast.get();
+
+        reset(mNetManager);
+
+        // now bring up wifi network
+        mWifi.info.setDetailedState(DetailedState.CONNECTED, null, null);
+        mWifi.link.setInterfaceName(WIFI_IFACE);
+        mWifi.link.addRoute(WIFI_ROUTE_V4);
+        mWifi.link.addRoute(WIFI_ROUTE_V6);
+        mWifi.doReturnDefaults();
+
+        // expect that mobile will be torn down
+        doReturn(true).when(mMobile.tracker).teardown();
+
+        nextConnBroadcast = mServiceContext.nextBroadcastIntent(CONNECTIVITY_ACTION_IMMEDIATE);
+        mTrackerHandler.obtainMessage(EVENT_STATE_CHANGED, mWifi.info).sendToTarget();
+        nextConnBroadcast.get();
+
+        // verify that wifi routes added, and teardown requested
+        int wifiNetId = mWifi.tracker.getNetwork().netId;
+        verify(mNetManager).addRoute(eq(wifiNetId), eq(WIFI_ROUTE_V4));
+        verify(mNetManager).addRoute(eq(wifiNetId), eq(WIFI_ROUTE_V6));
+        verify(mNetManager).flushNetworkDnsCache(wifiNetId);
+        verify(mMobile.tracker).teardown();
+
+        int mobileNetId = mMobile.tracker.getNetwork().netId;
+
+        reset(mNetManager, mMobile.tracker);
+
+        // tear down mobile network, as requested
+        mMobile.info.setDetailedState(DetailedState.DISCONNECTED, null, null);
+        mMobile.link.clear();
+        mMobile.doReturnDefaults();
+
+        nextConnBroadcast = mServiceContext.nextBroadcastIntent(CONNECTIVITY_ACTION_IMMEDIATE);
+        mTrackerHandler.obtainMessage(EVENT_STATE_CHANGED, mMobile.info).sendToTarget();
+        nextConnBroadcast.get();
+
+        verify(mNetManager).removeRoute(eq(mobileNetId), eq(MOBILE_ROUTE_V4));
+        verify(mNetManager).removeRoute(eq(mobileNetId), eq(MOBILE_ROUTE_V6));
+
+    }
+
+    private static InetAddress parse(String addr) {
+        return InetAddress.parseNumericAddress(addr);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java
new file mode 100644
index 0000000..0d5daa5
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.net.LinkAddress;
+import android.net.LocalSocket;
+import android.net.LocalServerSocket;
+import android.os.Binder;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import com.android.server.net.BaseNetworkObserver;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Tests for {@link NetworkManagementService}.
+ */
+@LargeTest
+public class NetworkManagementServiceTest extends AndroidTestCase {
+
+    private static final String SOCKET_NAME = "__test__NetworkManagementServiceTest";
+    private NetworkManagementService mNMService;
+    private LocalServerSocket mServerSocket;
+    private LocalSocket mSocket;
+    private OutputStream mOutputStream;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        // TODO: make this unnecessary. runtest might already make it unnecessary.
+        System.setProperty("dexmaker.dexcache", getContext().getCacheDir().toString());
+
+        // Set up a sheltered test environment.
+        BroadcastInterceptingContext context = new BroadcastInterceptingContext(getContext());
+        mServerSocket = new LocalServerSocket(SOCKET_NAME);
+
+        // Start the service and wait until it connects to our socket.
+        mNMService = NetworkManagementService.create(context, SOCKET_NAME);
+        mSocket = mServerSocket.accept();
+        mOutputStream = mSocket.getOutputStream();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        if (mSocket != null) mSocket.close();
+        if (mServerSocket != null) mServerSocket.close();
+        super.tearDown();
+    }
+
+    /**
+     * Sends a message on the netd socket and gives the events some time to make it back.
+     */
+    private void sendMessage(String message) throws IOException {
+        // Strings are null-terminated, so add "\0" at the end.
+        mOutputStream.write((message + "\0").getBytes());
+    }
+
+    private static <T> T expectSoon(T mock) {
+        return verify(mock, timeout(100));
+    }
+
+    /**
+     * Tests that network observers work properly.
+     */
+    public void testNetworkObservers() throws Exception {
+        BaseNetworkObserver observer = mock(BaseNetworkObserver.class);
+        doReturn(new Binder()).when(observer).asBinder();  // Used by registerObserver.
+        mNMService.registerObserver(observer);
+
+        // Forget everything that happened to the mock so far, so we can explicitly verify
+        // everything that happens and does not happen to it from now on.
+        reset(observer);
+
+        // Now send NetworkManagementService messages and ensure that the observer methods are
+        // called. After every valid message we expect a callback soon after; to ensure that
+        // invalid messages don't cause any callbacks, we call verifyNoMoreInteractions at the end.
+
+        /**
+         * Interface changes.
+         */
+        sendMessage("600 Iface added rmnet12");
+        expectSoon(observer).interfaceAdded("rmnet12");
+
+        sendMessage("600 Iface removed eth1");
+        expectSoon(observer).interfaceRemoved("eth1");
+
+        sendMessage("607 Iface removed eth1");
+        // Invalid code.
+
+        sendMessage("600 Iface borked lo down");
+        // Invalid event.
+
+        sendMessage("600 Iface changed clat4 up again");
+        // Extra tokens.
+
+        sendMessage("600 Iface changed clat4 up");
+        expectSoon(observer).interfaceStatusChanged("clat4", true);
+
+        sendMessage("600 Iface linkstate rmnet0 down");
+        expectSoon(observer).interfaceLinkStateChanged("rmnet0", false);
+
+        sendMessage("600 IFACE linkstate clat4 up");
+        // Invalid group.
+
+        /**
+         * Bandwidth control events.
+         */
+        sendMessage("601 limit alert data rmnet_usb0");
+        expectSoon(observer).limitReached("data", "rmnet_usb0");
+
+        sendMessage("601 invalid alert data rmnet0");
+        // Invalid group.
+
+        sendMessage("601 limit increased data rmnet0");
+        // Invalid event.
+
+
+        /**
+         * Interface class activity.
+         */
+
+        sendMessage("613 IfaceClass active rmnet0");
+        expectSoon(observer).interfaceClassDataActivityChanged("rmnet0", true, 0);
+
+        sendMessage("613 IfaceClass active rmnet0 1234");
+        expectSoon(observer).interfaceClassDataActivityChanged("rmnet0", true, 1234);
+
+        sendMessage("613 IfaceClass idle eth0");
+        expectSoon(observer).interfaceClassDataActivityChanged("eth0", false, 0);
+
+        sendMessage("613 IfaceClass idle eth0 1234");
+        expectSoon(observer).interfaceClassDataActivityChanged("eth0", false, 1234);
+
+        sendMessage("613 IfaceClass reallyactive rmnet0 1234");
+        expectSoon(observer).interfaceClassDataActivityChanged("rmnet0", false, 1234);
+
+        sendMessage("613 InterfaceClass reallyactive rmnet0");
+        // Invalid group.
+
+
+        /**
+         * IP address changes.
+         */
+        sendMessage("614 Address updated fe80::1/64 wlan0 128 253");
+        expectSoon(observer).addressUpdated("wlan0", new LinkAddress("fe80::1/64", 128, 253));
+
+        // There is no "added", so we take this as "removed".
+        sendMessage("614 Address added fe80::1/64 wlan0 128 253");
+        expectSoon(observer).addressRemoved("wlan0", new LinkAddress("fe80::1/64", 128, 253));
+
+        sendMessage("614 Address removed 2001:db8::1/64 wlan0 1 0");
+        expectSoon(observer).addressRemoved("wlan0", new LinkAddress("2001:db8::1/64", 1, 0));
+
+        sendMessage("614 Address removed 2001:db8::1/64 wlan0 1");
+        // Not enough arguments.
+
+        sendMessage("666 Address removed 2001:db8::1/64 wlan0 1 0");
+        // Invalid code.
+
+
+        /**
+         * DNS information broadcasts.
+         */
+        sendMessage("615 DnsInfo servers rmnet_usb0 3600 2001:db8::1");
+        expectSoon(observer).interfaceDnsServerInfo("rmnet_usb0", 3600,
+                new String[]{"2001:db8::1"});
+
+        sendMessage("615 DnsInfo servers wlan0 14400 2001:db8::1,2001:db8::2");
+        expectSoon(observer).interfaceDnsServerInfo("wlan0", 14400,
+                new String[]{"2001:db8::1", "2001:db8::2"});
+
+        // We don't check for negative lifetimes, only for parse errors.
+        sendMessage("615 DnsInfo servers wlan0 -3600 ::1");
+        expectSoon(observer).interfaceDnsServerInfo("wlan0", -3600,
+                new String[]{"::1"});
+
+        sendMessage("615 DnsInfo servers wlan0 SIXHUNDRED ::1");
+        // Non-numeric lifetime.
+
+        sendMessage("615 DnsInfo servers wlan0 2001:db8::1");
+        // Missing lifetime.
+
+        sendMessage("615 DnsInfo servers wlan0 3600");
+        // No servers.
+
+        sendMessage("615 DnsInfo servers 3600 wlan0 2001:db8::1,2001:db8::2");
+        // Non-numeric lifetime.
+
+        sendMessage("615 DnsInfo wlan0 7200 2001:db8::1,2001:db8::2");
+        // Invalid tokens.
+
+        sendMessage("666 DnsInfo servers wlan0 5400 2001:db8::1");
+        // Invalid code.
+
+        // No syntax checking on the addresses.
+        sendMessage("615 DnsInfo servers wlan0 600 ,::,,foo,::1,");
+        expectSoon(observer).interfaceDnsServerInfo("wlan0", 600,
+                new String[]{"", "::", "", "foo", "::1"});
+
+        // Make sure nothing else was called.
+        verifyNoMoreInteractions(observer);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
new file mode 100644
index 0000000..a1af8cb
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/NetworkStatsServiceTest.java
@@ -0,0 +1,1057 @@
+/*
+ * 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;
+
+import static android.content.Intent.ACTION_UID_REMOVED;
+import static android.content.Intent.EXTRA_UID;
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION_IMMEDIATE;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivityManager.TYPE_WIMAX;
+import static android.net.NetworkStats.IFACE_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.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.NetworkStatsHistory.FIELD_ALL;
+import static android.net.NetworkTemplate.buildTemplateMobileAll;
+import static android.net.NetworkTemplate.buildTemplateWifiWildcard;
+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.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.WEEK_IN_MILLIS;
+import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static org.easymock.EasyMock.anyLong;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.isA;
+
+import android.app.AlarmManager;
+import android.app.IAlarmManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.net.IConnectivityManager;
+import android.net.INetworkManagementEventObserver;
+import android.net.INetworkStatsSession;
+import android.net.LinkProperties;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkState;
+import android.net.NetworkStats;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.os.INetworkManagementService;
+import android.os.WorkSource;
+import android.telephony.TelephonyManager;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.util.TrustedTime;
+
+import com.android.server.net.NetworkStatsService;
+import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
+import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config;
+
+import libcore.io.IoUtils;
+
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+
+import java.io.File;
+
+/**
+ * Tests for {@link NetworkStatsService}.
+ */
+@LargeTest
+public class NetworkStatsServiceTest extends AndroidTestCase {
+    private static final String TAG = "NetworkStatsServiceTest";
+
+    private static final String TEST_IFACE = "test0";
+    private static final String TEST_IFACE2 = "test1";
+    private static final long TEST_START = 1194220800000L;
+
+    private static final String IMSI_1 = "310004";
+    private static final String IMSI_2 = "310260";
+    private static final String TEST_SSID = "AndroidAP";
+
+    private static NetworkTemplate sTemplateWifi = buildTemplateWifiWildcard();
+    private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1);
+    private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2);
+
+    private static final int UID_RED = 1001;
+    private static final int UID_BLUE = 1002;
+    private static final int UID_GREEN = 1003;
+
+    private long mElapsedRealtime;
+
+    private BroadcastInterceptingContext mServiceContext;
+    private File mStatsDir;
+
+    private INetworkManagementService mNetManager;
+    private IAlarmManager mAlarmManager;
+    private TrustedTime mTime;
+    private NetworkStatsSettings mSettings;
+    private IConnectivityManager mConnManager;
+
+    private NetworkStatsService mService;
+    private INetworkStatsSession mSession;
+    private INetworkManagementEventObserver mNetworkObserver;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mServiceContext = new BroadcastInterceptingContext(getContext());
+        mStatsDir = getContext().getFilesDir();
+        if (mStatsDir.exists()) {
+            IoUtils.deleteContents(mStatsDir);
+        }
+
+        mNetManager = createMock(INetworkManagementService.class);
+        mAlarmManager = createMock(IAlarmManager.class);
+        mTime = createMock(TrustedTime.class);
+        mSettings = createMock(NetworkStatsSettings.class);
+        mConnManager = createMock(IConnectivityManager.class);
+
+        mService = new NetworkStatsService(
+                mServiceContext, mNetManager, mAlarmManager, mTime, mStatsDir, mSettings);
+        mService.bindConnectivityManager(mConnManager);
+
+        mElapsedRealtime = 0L;
+
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+
+        // catch INetworkManagementEventObserver during systemReady()
+        final Capture<INetworkManagementEventObserver> networkObserver = new Capture<
+                INetworkManagementEventObserver>();
+        mNetManager.registerObserver(capture(networkObserver));
+        expectLastCall().atLeastOnce();
+
+        replay();
+        mService.systemReady();
+        mSession = mService.openSession();
+        verifyAndReset();
+
+        mNetworkObserver = networkObserver.getValue();
+
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        IoUtils.deleteContents(mStatsDir);
+
+        mServiceContext = null;
+        mStatsDir = null;
+
+        mNetManager = null;
+        mAlarmManager = null;
+        mTime = null;
+        mSettings = null;
+        mConnManager = null;
+
+        mSession.close();
+        mService = null;
+
+        super.tearDown();
+    }
+
+    public void testNetworkStatsWifi() throws Exception {
+        // pretend that wifi network comes online; service should ask about full
+        // network state, and poll any existing interfaces before updating.
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkState(buildWifiState());
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE));
+
+        // verify service has empty history for wifi
+        assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
+        verifyAndReset();
+
+        // modify some number on wifi, and trigger poll event
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 1024L, 1L, 2048L, 2L));
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0);
+        verifyAndReset();
+
+        // and bump forward again, with counters going higher. this is
+        // important, since polling should correctly subtract last snapshot.
+        incrementCurrentTime(DAY_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 4096L, 4L, 8192L, 8L));
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateWifi, 4096L, 4L, 8192L, 8L, 0);
+        verifyAndReset();
+
+    }
+
+    public void testStatsRebootPersist() throws Exception {
+        assertStatsFilesExist(false);
+
+        // pretend that wifi network comes online; service should ask about full
+        // network state, and poll any existing interfaces before updating.
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkState(buildWifiState());
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE));
+
+        // verify service has empty history for wifi
+        assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
+        verifyAndReset();
+
+        // modify some number on wifi, and trigger poll event
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 1024L, 8L, 2048L, 16L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
+                .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
+        expectNetworkStatsPoll();
+
+        mService.setUidForeground(UID_RED, false);
+        mService.incrementOperationCount(UID_RED, 0xFAAD, 4);
+        mService.setUidForeground(UID_RED, true);
+        mService.incrementOperationCount(UID_RED, 0xFAAD, 6);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0);
+        assertUidTotal(sTemplateWifi, UID_RED, 1024L, 8L, 512L, 4L, 10);
+        assertUidTotal(sTemplateWifi, UID_RED, SET_DEFAULT, 512L, 4L, 256L, 2L, 4);
+        assertUidTotal(sTemplateWifi, UID_RED, SET_FOREGROUND, 512L, 4L, 256L, 2L, 6);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 128L, 1L, 128L, 1L, 0);
+        verifyAndReset();
+
+        // graceful shutdown system, which should trigger persist of stats, and
+        // clear any values in memory.
+        expectCurrentTime();
+        expectDefaultSettings();
+        replay();
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+        verifyAndReset();
+
+        assertStatsFilesExist(true);
+
+        // boot through serviceReady() again
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+
+        // catch INetworkManagementEventObserver during systemReady()
+        final Capture<INetworkManagementEventObserver> networkObserver = new Capture<
+                INetworkManagementEventObserver>();
+        mNetManager.registerObserver(capture(networkObserver));
+        expectLastCall().atLeastOnce();
+
+        replay();
+        mService.systemReady();
+
+        mNetworkObserver = networkObserver.getValue();
+
+        // after systemReady(), we should have historical stats loaded again
+        assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0);
+        assertUidTotal(sTemplateWifi, UID_RED, 1024L, 8L, 512L, 4L, 10);
+        assertUidTotal(sTemplateWifi, UID_RED, SET_DEFAULT, 512L, 4L, 256L, 2L, 4);
+        assertUidTotal(sTemplateWifi, UID_RED, SET_FOREGROUND, 512L, 4L, 256L, 2L, 6);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 128L, 1L, 128L, 1L, 0);
+        verifyAndReset();
+
+    }
+
+    // TODO: simulate reboot to test bucket resize
+    @Suppress
+    public void testStatsBucketResize() throws Exception {
+        NetworkStatsHistory history = null;
+
+        assertStatsFilesExist(false);
+
+        // pretend that wifi network comes online; service should ask about full
+        // network state, and poll any existing interfaces before updating.
+        expectCurrentTime();
+        expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+        expectNetworkState(buildWifiState());
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE));
+        verifyAndReset();
+
+        // modify some number on wifi, and trigger poll event
+        incrementCurrentTime(2 * HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 512L, 4L, 512L, 4L));
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        history = mSession.getHistoryForNetwork(sTemplateWifi, FIELD_ALL);
+        assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, 512L, 4L, 512L, 4L, 0);
+        assertEquals(HOUR_IN_MILLIS, history.getBucketDuration());
+        assertEquals(2, history.size());
+        verifyAndReset();
+
+        // now change bucket duration setting and trigger another poll with
+        // exact same values, which should resize existing buckets.
+        expectCurrentTime();
+        expectSettings(0L, 30 * MINUTE_IN_MILLIS, WEEK_IN_MILLIS);
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify identical stats, but spread across 4 buckets now
+        history = mSession.getHistoryForNetwork(sTemplateWifi, FIELD_ALL);
+        assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, 512L, 4L, 512L, 4L, 0);
+        assertEquals(30 * MINUTE_IN_MILLIS, history.getBucketDuration());
+        assertEquals(4, history.size());
+        verifyAndReset();
+
+    }
+
+    public void testUidStatsAcrossNetworks() throws Exception {
+        // pretend first mobile network comes online
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkState(buildMobile3gState(IMSI_1));
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE));
+        verifyAndReset();
+
+        // create some traffic on first network
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 2048L, 16L, 512L, 4L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
+                .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L));
+        expectNetworkStatsPoll();
+
+        mService.incrementOperationCount(UID_RED, 0xF00D, 10);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0);
+        assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
+        assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 512L, 4L, 10);
+        assertUidTotal(sTemplateImsi1, UID_BLUE, 512L, 4L, 0L, 0L, 0);
+        verifyAndReset();
+
+        // now switch networks; this also tests that we're okay with interfaces
+        // disappearing, to verify we don't count backwards.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkState(buildMobile3gState(IMSI_2));
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 2048L, 16L, 512L, 4L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
+                .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L));
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE));
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+        verifyAndReset();
+
+        // create traffic on second network
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 2176L, 17L, 1536L, 12L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
+                .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 640L, 5L, 1024L, 8L, 0L)
+                .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xFAAD, 128L, 1L, 1024L, 8L, 0L));
+        expectNetworkStatsPoll();
+
+        mService.incrementOperationCount(UID_BLUE, 0xFAAD, 10);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify original history still intact
+        assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0);
+        assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 512L, 4L, 10);
+        assertUidTotal(sTemplateImsi1, UID_BLUE, 512L, 4L, 0L, 0L, 0);
+
+        // and verify new history also recorded under different template, which
+        // verifies that we didn't cross the streams.
+        assertNetworkTotal(sTemplateImsi2, 128L, 1L, 1024L, 8L, 0);
+        assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
+        assertUidTotal(sTemplateImsi2, UID_BLUE, 128L, 1L, 1024L, 8L, 10);
+        verifyAndReset();
+
+    }
+
+    public void testUidRemovedIsMoved() throws Exception {
+        // pretend that network comes online
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkState(buildWifiState());
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE));
+        verifyAndReset();
+
+        // create some traffic
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 4128L, 258L, 544L, 34L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 16L, 1L, 16L, 1L, 0L)
+                .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 4096L, 258L, 512L, 32L, 0L)
+                .addValues(TEST_IFACE, UID_GREEN, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L));
+        expectNetworkStatsPoll();
+
+        mService.incrementOperationCount(UID_RED, 0xFAAD, 10);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateWifi, 4128L, 258L, 544L, 34L, 0);
+        assertUidTotal(sTemplateWifi, UID_RED, 16L, 1L, 16L, 1L, 10);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 4096L, 258L, 512L, 32L, 0);
+        assertUidTotal(sTemplateWifi, UID_GREEN, 16L, 1L, 16L, 1L, 0);
+        verifyAndReset();
+
+        // now pretend two UIDs are uninstalled, which should migrate stats to
+        // special "removed" bucket.
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 4128L, 258L, 544L, 34L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 16L, 1L, 16L, 1L, 0L)
+                .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 4096L, 258L, 512L, 32L, 0L)
+                .addValues(TEST_IFACE, UID_GREEN, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L));
+        expectNetworkStatsPoll();
+
+        replay();
+        final Intent intent = new Intent(ACTION_UID_REMOVED);
+        intent.putExtra(EXTRA_UID, UID_BLUE);
+        mServiceContext.sendBroadcast(intent);
+        intent.putExtra(EXTRA_UID, UID_RED);
+        mServiceContext.sendBroadcast(intent);
+
+        // existing uid and total should remain unchanged; but removed UID
+        // should be gone completely.
+        assertNetworkTotal(sTemplateWifi, 4128L, 258L, 544L, 34L, 0);
+        assertUidTotal(sTemplateWifi, UID_RED, 0L, 0L, 0L, 0L, 0);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 0L, 0L, 0L, 0L, 0);
+        assertUidTotal(sTemplateWifi, UID_GREEN, 16L, 1L, 16L, 1L, 0);
+        assertUidTotal(sTemplateWifi, UID_REMOVED, 4112L, 259L, 528L, 33L, 10);
+        verifyAndReset();
+
+    }
+
+    public void testUid3g4gCombinedByTemplate() throws Exception {
+        // pretend that network comes online
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkState(buildMobile3gState(IMSI_1));
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE));
+        verifyAndReset();
+
+        // create some traffic
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L));
+        expectNetworkStatsPoll();
+
+        mService.incrementOperationCount(UID_RED, 0xF00D, 5);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        assertUidTotal(sTemplateImsi1, UID_RED, 1024L, 8L, 1024L, 8L, 5);
+        verifyAndReset();
+
+        // now switch over to 4g network
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkState(buildMobile4gState(TEST_IFACE2));
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L));
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE));
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+        verifyAndReset();
+
+        // create traffic on second network
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
+                .addValues(TEST_IFACE2, UID_RED, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
+                .addValues(TEST_IFACE2, UID_RED, SET_DEFAULT, 0xFAAD, 512L, 4L, 256L, 2L, 0L));
+        expectNetworkStatsPoll();
+
+        mService.incrementOperationCount(UID_RED, 0xFAAD, 5);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify that ALL_MOBILE template combines both
+        assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 1280L, 10L, 10);
+
+        verifyAndReset();
+    }
+
+    public void testSummaryForAllUid() throws Exception {
+        // pretend that network comes online
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkState(buildWifiState());
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE));
+        verifyAndReset();
+
+        // create some traffic for two apps
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 0L)
+                .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L));
+        expectNetworkStatsPoll();
+
+        mService.incrementOperationCount(UID_RED, 0xF00D, 1);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        assertUidTotal(sTemplateWifi, UID_RED, 50L, 5L, 50L, 5L, 1);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 1024L, 8L, 512L, 4L, 0);
+        verifyAndReset();
+
+        // now create more traffic in next hour, but only for one app
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 0L)
+                .addValues(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 2048L, 16L, 1024L, 8L, 0L));
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // first verify entire history present
+        NetworkStats stats = mSession.getSummaryForAllUid(
+                sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true);
+        assertEquals(3, stats.size());
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 1);
+        assertValues(stats, IFACE_ALL, UID_BLUE, SET_DEFAULT, TAG_NONE, 2048L, 16L, 1024L, 8L, 0);
+
+        // now verify that recent history only contains one uid
+        final long currentTime = currentTimeMillis();
+        stats = mSession.getSummaryForAllUid(
+                sTemplateWifi, currentTime - HOUR_IN_MILLIS, currentTime, true);
+        assertEquals(1, stats.size());
+        assertValues(stats, IFACE_ALL, UID_BLUE, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0);
+
+        verifyAndReset();
+    }
+
+    public void testForegroundBackground() throws Exception {
+        // pretend that network comes online
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkState(buildWifiState());
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE));
+        verifyAndReset();
+
+        // create some initial traffic
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L));
+        expectNetworkStatsPoll();
+
+        mService.incrementOperationCount(UID_RED, 0xF00D, 1);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        assertUidTotal(sTemplateWifi, UID_RED, 128L, 2L, 128L, 2L, 1);
+        verifyAndReset();
+
+        // now switch to foreground
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 32L, 2L, 32L, 2L, 0L)
+                .addValues(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 1L, 1L, 1L, 1L, 0L));
+        expectNetworkStatsPoll();
+
+        mService.setUidForeground(UID_RED, true);
+        mService.incrementOperationCount(UID_RED, 0xFAAD, 1);
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // test that we combined correctly
+        assertUidTotal(sTemplateWifi, UID_RED, 160L, 4L, 160L, 4L, 2);
+
+        // verify entire history present
+        final NetworkStats stats = mSession.getSummaryForAllUid(
+                sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true);
+        assertEquals(4, stats.size());
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_FOREGROUND, TAG_NONE, 32L, 2L, 32L, 2L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_FOREGROUND, 0xFAAD, 1L, 1L, 1L, 1L, 1);
+
+        verifyAndReset();
+    }
+
+    public void testTethering() throws Exception {
+        // pretend first mobile network comes online
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkState(buildMobile3gState(IMSI_1));
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE));
+        verifyAndReset();
+
+        // create some tethering traffic
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 2048L, 16L, 512L, 4L));
+
+        final NetworkStats uidStats = new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L);
+        final String[] tetherIfacePairs = new String[] { TEST_IFACE, "wlan0" };
+        final NetworkStats tetherStats = new NetworkStats(getElapsedRealtime(), 1)
+                .addValues(TEST_IFACE, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1920L, 14L, 384L, 2L, 0L);
+
+        expectNetworkStatsUidDetail(uidStats, tetherIfacePairs, tetherStats);
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0);
+        assertUidTotal(sTemplateImsi1, UID_RED, 128L, 2L, 128L, 2L, 0);
+        assertUidTotal(sTemplateImsi1, UID_TETHERING, 1920L, 14L, 384L, 2L, 0);
+        verifyAndReset();
+
+    }
+
+    public void testReportXtOverDev() throws Exception {
+        // bring mobile network online
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkState(buildMobile3gState(IMSI_1));
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(CONNECTIVITY_ACTION_IMMEDIATE));
+        verifyAndReset();
+
+        // create some traffic, but only for DEV, and across 1.5 buckets
+        incrementCurrentTime(90 * MINUTE_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummaryDev(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 6000L, 60L, 3000L, 30L));
+        expectNetworkStatsSummaryXt(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify service recorded history:
+        // 4000(dev) + 2000(dev)
+        assertNetworkTotal(sTemplateImsi1, 6000L, 60L, 3000L, 30L, 0);
+        verifyAndReset();
+
+        // create traffic on both DEV and XT, across two buckets
+        incrementCurrentTime(2 * HOUR_IN_MILLIS);
+        expectCurrentTime();
+        expectDefaultSettings();
+        expectNetworkStatsSummaryDev(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 6004L, 64L, 3004L, 34L));
+        expectNetworkStatsSummaryXt(new NetworkStats(getElapsedRealtime(), 1)
+                .addIfaceValues(TEST_IFACE, 10240L, 0L, 0L, 0L));
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectNetworkStatsPoll();
+
+        replay();
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+
+        // verify that we switching reporting at the first atomic XT bucket,
+        // which should give us:
+        // 4000(dev) + 2000(dev) + 1(dev) + 5120(xt) + 2560(xt)
+        assertNetworkTotal(sTemplateImsi1, 13681L, 61L, 3001L, 31L, 0);
+
+        // also test pure-DEV and pure-XT ranges
+        assertNetworkTotal(sTemplateImsi1, startTimeMillis(),
+                startTimeMillis() + 2 * HOUR_IN_MILLIS, 6001L, 61L, 3001L, 31L, 0);
+        assertNetworkTotal(sTemplateImsi1, startTimeMillis() + 2 * HOUR_IN_MILLIS,
+                startTimeMillis() + 4 * HOUR_IN_MILLIS, 7680L, 0L, 0L, 0L, 0);
+
+        verifyAndReset();
+    }
+
+    private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets,
+            long txBytes, long txPackets, int operations) throws Exception {
+        assertNetworkTotal(template, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes,
+                txPackets, operations);
+    }
+
+    private void assertNetworkTotal(NetworkTemplate template, long start, long end, long rxBytes,
+            long rxPackets, long txBytes, long txPackets, int operations) throws Exception {
+        // verify history API
+        final NetworkStatsHistory history = mSession.getHistoryForNetwork(template, FIELD_ALL);
+        assertValues(history, start, end, rxBytes, rxPackets, txBytes, txPackets, operations);
+
+        // verify summary API
+        final NetworkStats stats = mSession.getSummaryForNetwork(template, start, end);
+        assertValues(stats, IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, rxPackets, txBytes,
+                txPackets, operations);
+    }
+
+    private void assertUidTotal(NetworkTemplate template, int uid, long rxBytes, long rxPackets,
+            long txBytes, long txPackets, int operations) throws Exception {
+        assertUidTotal(template, uid, SET_ALL, rxBytes, rxPackets, txBytes, txPackets, operations);
+    }
+
+    private void assertUidTotal(NetworkTemplate template, int uid, int set, long rxBytes,
+            long rxPackets, long txBytes, long txPackets, int operations) throws Exception {
+        // verify history API
+        final NetworkStatsHistory history = mSession.getHistoryForUid(
+                template, uid, set, TAG_NONE, FIELD_ALL);
+        assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes,
+                txPackets, operations);
+
+        // verify summary API
+        final NetworkStats stats = mSession.getSummaryForAllUid(
+                template, Long.MIN_VALUE, Long.MAX_VALUE, false);
+        assertValues(stats, IFACE_ALL, uid, set, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets,
+                operations);
+    }
+
+    private void expectSystemReady() throws Exception {
+        mAlarmManager.remove(isA(PendingIntent.class));
+        expectLastCall().anyTimes();
+
+        mAlarmManager.set(eq(AlarmManager.ELAPSED_REALTIME), anyLong(), anyLong(), anyLong(),
+                isA(PendingIntent.class), isA(WorkSource.class));
+        expectLastCall().atLeastOnce();
+
+        mNetManager.setGlobalAlert(anyLong());
+        expectLastCall().atLeastOnce();
+
+        expect(mNetManager.isBandwidthControlEnabled()).andReturn(true).atLeastOnce();
+    }
+
+    private void expectNetworkState(NetworkState... state) throws Exception {
+        expect(mConnManager.getAllNetworkState()).andReturn(state).atLeastOnce();
+
+        final LinkProperties linkProp = state.length > 0 ? state[0].linkProperties : null;
+        expect(mConnManager.getActiveLinkProperties()).andReturn(linkProp).atLeastOnce();
+    }
+
+    private void expectNetworkStatsSummary(NetworkStats summary) throws Exception {
+        expectNetworkStatsSummaryDev(summary);
+        expectNetworkStatsSummaryXt(summary);
+    }
+
+    private void expectNetworkStatsSummaryDev(NetworkStats summary) throws Exception {
+        expect(mNetManager.getNetworkStatsSummaryDev()).andReturn(summary).atLeastOnce();
+    }
+
+    private void expectNetworkStatsSummaryXt(NetworkStats summary) throws Exception {
+        expect(mNetManager.getNetworkStatsSummaryXt()).andReturn(summary).atLeastOnce();
+    }
+
+    private void expectNetworkStatsUidDetail(NetworkStats detail) throws Exception {
+        expectNetworkStatsUidDetail(detail, new String[0], new NetworkStats(0L, 0));
+    }
+
+    private void expectNetworkStatsUidDetail(
+            NetworkStats detail, String[] tetherIfacePairs, NetworkStats tetherStats)
+            throws Exception {
+        expect(mNetManager.getNetworkStatsUidDetail(eq(UID_ALL))).andReturn(detail).atLeastOnce();
+
+        // also include tethering details, since they are folded into UID
+        expect(mNetManager.getNetworkStatsTethering())
+                .andReturn(tetherStats).atLeastOnce();
+    }
+
+    private void expectDefaultSettings() throws Exception {
+        expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+    }
+
+    private void expectSettings(long persistBytes, long bucketDuration, long deleteAge)
+            throws Exception {
+        expect(mSettings.getPollInterval()).andReturn(HOUR_IN_MILLIS).anyTimes();
+        expect(mSettings.getTimeCacheMaxAge()).andReturn(DAY_IN_MILLIS).anyTimes();
+        expect(mSettings.getSampleEnabled()).andReturn(true).anyTimes();
+        expect(mSettings.getReportXtOverDev()).andReturn(true).anyTimes();
+
+        final Config config = new Config(bucketDuration, deleteAge, deleteAge);
+        expect(mSettings.getDevConfig()).andReturn(config).anyTimes();
+        expect(mSettings.getXtConfig()).andReturn(config).anyTimes();
+        expect(mSettings.getUidConfig()).andReturn(config).anyTimes();
+        expect(mSettings.getUidTagConfig()).andReturn(config).anyTimes();
+
+        expect(mSettings.getGlobalAlertBytes(anyLong())).andReturn(MB_IN_BYTES).anyTimes();
+        expect(mSettings.getDevPersistBytes(anyLong())).andReturn(MB_IN_BYTES).anyTimes();
+        expect(mSettings.getXtPersistBytes(anyLong())).andReturn(MB_IN_BYTES).anyTimes();
+        expect(mSettings.getUidPersistBytes(anyLong())).andReturn(MB_IN_BYTES).anyTimes();
+        expect(mSettings.getUidTagPersistBytes(anyLong())).andReturn(MB_IN_BYTES).anyTimes();
+    }
+
+    private void expectCurrentTime() throws Exception {
+        expect(mTime.forceRefresh()).andReturn(false).anyTimes();
+        expect(mTime.hasCache()).andReturn(true).anyTimes();
+        expect(mTime.currentTimeMillis()).andReturn(currentTimeMillis()).anyTimes();
+        expect(mTime.getCacheAge()).andReturn(0L).anyTimes();
+        expect(mTime.getCacheCertainty()).andReturn(0L).anyTimes();
+    }
+
+    private void expectNetworkStatsPoll() throws Exception {
+        mNetManager.setGlobalAlert(anyLong());
+        expectLastCall().anyTimes();
+    }
+
+    private void assertStatsFilesExist(boolean exist) {
+        final File basePath = new File(mStatsDir, "netstats");
+        if (exist) {
+            assertTrue(basePath.list().length > 0);
+        } else {
+            assertTrue(basePath.list().length == 0);
+        }
+    }
+
+    private static void assertValues(NetworkStats stats, String iface, int uid, int set,
+            int tag, long rxBytes, long rxPackets, long txBytes, long txPackets, int operations) {
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        if (set == SET_DEFAULT || set == SET_ALL) {
+            final int i = stats.findIndex(iface, uid, SET_DEFAULT, tag);
+            if (i != -1) {
+                entry.add(stats.getValues(i, null));
+            }
+        }
+        if (set == SET_FOREGROUND || set == SET_ALL) {
+            final int i = stats.findIndex(iface, uid, SET_FOREGROUND, tag);
+            if (i != -1) {
+                entry.add(stats.getValues(i, null));
+            }
+        }
+
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+        assertEquals("unexpected txPackets", txPackets, entry.txPackets);
+        assertEquals("unexpected operations", operations, entry.operations);
+    }
+
+    private static void assertValues(NetworkStatsHistory stats, long start, long end, long rxBytes,
+            long rxPackets, long txBytes, long txPackets, int operations) {
+        final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+        assertEquals("unexpected txPackets", txPackets, entry.txPackets);
+        assertEquals("unexpected operations", operations, entry.operations);
+    }
+
+    private static NetworkState buildWifiState() {
+        final NetworkInfo info = new NetworkInfo(TYPE_WIFI, 0, null, null);
+        info.setDetailedState(DetailedState.CONNECTED, null, null);
+        final LinkProperties prop = new LinkProperties();
+        prop.setInterfaceName(TEST_IFACE);
+        return new NetworkState(info, prop, null, null, TEST_SSID);
+    }
+
+    private static NetworkState buildMobile3gState(String subscriberId) {
+        final NetworkInfo info = new NetworkInfo(
+                TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UMTS, null, null);
+        info.setDetailedState(DetailedState.CONNECTED, null, null);
+        final LinkProperties prop = new LinkProperties();
+        prop.setInterfaceName(TEST_IFACE);
+        return new NetworkState(info, prop, null, subscriberId, null);
+    }
+
+    private static NetworkState buildMobile4gState(String iface) {
+        final NetworkInfo info = new NetworkInfo(TYPE_WIMAX, 0, null, null);
+        info.setDetailedState(DetailedState.CONNECTED, null, null);
+        final LinkProperties prop = new LinkProperties();
+        prop.setInterfaceName(iface);
+        return new NetworkState(info, prop, null);
+    }
+
+    private NetworkStats buildEmptyStats() {
+        return new NetworkStats(getElapsedRealtime(), 0);
+    }
+
+    private long getElapsedRealtime() {
+        return mElapsedRealtime;
+    }
+
+    private long startTimeMillis() {
+        return TEST_START;
+    }
+
+    private long currentTimeMillis() {
+        return startTimeMillis() + mElapsedRealtime;
+    }
+
+    private void incrementCurrentTime(long duration) {
+        mElapsedRealtime += duration;
+    }
+
+    private void replay() {
+        EasyMock.replay(mNetManager, mAlarmManager, mTime, mSettings, mConnManager);
+    }
+
+    private void verifyAndReset() {
+        EasyMock.verify(mNetManager, mAlarmManager, mTime, mSettings, mConnManager);
+        EasyMock.reset(mNetManager, mAlarmManager, mTime, mSettings, mConnManager);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java
new file mode 100644
index 0000000..1a6c289
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkStatsCollectionTest.java
@@ -0,0 +1,193 @@
+/*
+ * 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.SET_DEFAULT;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.NetworkTemplate.buildTemplateMobileAll;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+
+import android.content.res.Resources;
+import android.net.NetworkStats;
+import android.net.NetworkTemplate;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.frameworks.servicestests.R;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+/**
+ * Tests for {@link NetworkStatsCollection}.
+ */
+@MediumTest
+public class NetworkStatsCollectionTest extends AndroidTestCase {
+
+    private static final String TEST_FILE = "test.bin";
+    private static final String TEST_IMSI = "310260000000000";
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        // ignore any device overlay while testing
+        NetworkTemplate.forceAllNetworkTypes();
+    }
+
+    public void testReadLegacyNetwork() throws Exception {
+        final File testFile = new File(getContext().getFilesDir(), TEST_FILE);
+        stageFile(R.raw.netstats_v1, testFile);
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+        collection.readLegacyNetwork(testFile);
+
+        // verify that history read correctly
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                636016770L, 709306L, 88038768L, 518836L);
+
+        // now export into a unified format
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        collection.write(new DataOutputStream(bos));
+
+        // clear structure completely
+        collection.reset();
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                0L, 0L, 0L, 0L);
+
+        // and read back into structure, verifying that totals are same
+        collection.read(new ByteArrayInputStream(bos.toByteArray()));
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                636016770L, 709306L, 88038768L, 518836L);
+    }
+
+    public void testReadLegacyUid() throws Exception {
+        final File testFile = new File(getContext().getFilesDir(), TEST_FILE);
+        stageFile(R.raw.netstats_uid_v4, testFile);
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+        collection.readLegacyUid(testFile, false);
+
+        // verify that history read correctly
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                637076152L, 711413L, 88343717L, 521022L);
+
+        // now export into a unified format
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        collection.write(new DataOutputStream(bos));
+
+        // clear structure completely
+        collection.reset();
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                0L, 0L, 0L, 0L);
+
+        // and read back into structure, verifying that totals are same
+        collection.read(new ByteArrayInputStream(bos.toByteArray()));
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                637076152L, 711413L, 88343717L, 521022L);
+    }
+
+    public void testReadLegacyUidTags() throws Exception {
+        final File testFile = new File(getContext().getFilesDir(), TEST_FILE);
+        stageFile(R.raw.netstats_uid_v4, testFile);
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+        collection.readLegacyUid(testFile, true);
+
+        // verify that history read correctly
+        assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI),
+                77017831L, 100995L, 35436758L, 92344L);
+
+        // now export into a unified format
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        collection.write(new DataOutputStream(bos));
+
+        // clear structure completely
+        collection.reset();
+        assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI),
+                0L, 0L, 0L, 0L);
+
+        // and read back into structure, verifying that totals are same
+        collection.read(new ByteArrayInputStream(bos.toByteArray()));
+        assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI),
+                77017831L, 100995L, 35436758L, 92344L);
+    }
+
+    public void testStartEndAtomicBuckets() throws Exception {
+        final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS);
+
+        // record empty data straddling between buckets
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        entry.rxBytes = 32;
+        collection.recordData(null, UID_ALL, SET_DEFAULT, TAG_NONE, 30 * MINUTE_IN_MILLIS,
+                90 * MINUTE_IN_MILLIS, entry);
+
+        // assert that we report boundary in atomic buckets
+        assertEquals(0, collection.getStartMillis());
+        assertEquals(2 * HOUR_IN_MILLIS, collection.getEndMillis());
+    }
+
+    /**
+     * Copy a {@link Resources#openRawResource(int)} into {@link File} for
+     * testing purposes.
+     */
+    private void stageFile(int rawId, File file) throws Exception {
+        new File(file.getParent()).mkdirs();
+        InputStream in = null;
+        OutputStream out = null;
+        try {
+            in = getContext().getResources().openRawResource(rawId);
+            out = new FileOutputStream(file);
+            Streams.copy(in, out);
+        } finally {
+            IoUtils.closeQuietly(in);
+            IoUtils.closeQuietly(out);
+        }
+    }
+
+    private static void assertSummaryTotal(NetworkStatsCollection collection,
+            NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+        final NetworkStats.Entry entry = collection.getSummary(
+                template, Long.MIN_VALUE, Long.MAX_VALUE).getTotal(null);
+        assertEntry(entry, rxBytes, rxPackets, txBytes, txPackets);
+    }
+
+    private static void assertSummaryTotalIncludingTags(NetworkStatsCollection collection,
+            NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+        final NetworkStats.Entry entry = collection.getSummary(
+                template, Long.MIN_VALUE, Long.MAX_VALUE).getTotalIncludingTags(null);
+        assertEntry(entry, rxBytes, rxPackets, txBytes, txPackets);
+    }
+
+    private static void assertEntry(
+            NetworkStats.Entry entry, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+        assertEquals("unexpected txPackets", txPackets, entry.txPackets);
+    }
+}
diff --git a/tests/CoreTests/android/core/NsdServiceInfoTest.java b/tests/CoreTests/android/core/NsdServiceInfoTest.java
new file mode 100644
index 0000000..5bf0167
--- /dev/null
+++ b/tests/CoreTests/android/core/NsdServiceInfoTest.java
@@ -0,0 +1,163 @@
+package android.core;
+
+import android.test.AndroidTestCase;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.StrictMode;
+import android.net.nsd.NsdServiceInfo;
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+
+public class NsdServiceInfoTest extends AndroidTestCase {
+
+    public final static InetAddress LOCALHOST;
+    static {
+        // Because test.
+        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
+        StrictMode.setThreadPolicy(policy);
+
+        InetAddress _host = null;
+        try {
+            _host = InetAddress.getLocalHost();
+        } catch (UnknownHostException e) { }
+        LOCALHOST = _host;
+    }
+
+    public void testLimits() throws Exception {
+        NsdServiceInfo info = new NsdServiceInfo();
+
+        // Non-ASCII keys.
+        boolean exceptionThrown = false;
+        try {
+            info.setAttribute("猫", "meow");
+        } catch (IllegalArgumentException e) {
+            exceptionThrown = true;
+        }
+        assertTrue(exceptionThrown);
+        assertEmptyServiceInfo(info);
+
+        // ASCII keys with '=' character.
+        exceptionThrown = false;
+        try {
+            info.setAttribute("kitten=", "meow");
+        } catch (IllegalArgumentException e) {
+            exceptionThrown = true;
+        }
+        assertTrue(exceptionThrown);
+        assertEmptyServiceInfo(info);
+
+        // Single key + value length too long.
+        exceptionThrown = false;
+        try {
+            String longValue = "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" +
+                    "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" +
+                    "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" +
+                    "ooooooooooooooooooooooooooooong";  // 248 characters.
+            info.setAttribute("longcat", longValue);  // Key + value == 255 characters.
+        } catch (IllegalArgumentException e) {
+            exceptionThrown = true;
+        }
+        assertTrue(exceptionThrown);
+        assertEmptyServiceInfo(info);
+
+        // Total TXT record length too long.
+        exceptionThrown = false;
+        int recordsAdded = 0;
+        try {
+            for (int i = 100; i < 300; ++i) {
+                // 6 char key + 5 char value + 2 bytes overhead = 13 byte record length.
+                String key = String.format("key%d", i);
+                info.setAttribute(key, "12345");
+                recordsAdded++;
+            }
+        } catch (IllegalArgumentException e) {
+            exceptionThrown = true;
+        }
+        assertTrue(exceptionThrown);
+        assertTrue(100 == recordsAdded);
+        assertTrue(info.getTxtRecord().length == 1300);
+    }
+
+    public void testParcel() throws Exception {
+        NsdServiceInfo emptyInfo = new NsdServiceInfo();
+        checkParcelable(emptyInfo);
+
+        NsdServiceInfo fullInfo = new NsdServiceInfo();
+        fullInfo.setServiceName("kitten");
+        fullInfo.setServiceType("_kitten._tcp");
+        fullInfo.setPort(4242);
+        fullInfo.setHost(LOCALHOST);
+        checkParcelable(fullInfo);
+
+        NsdServiceInfo noHostInfo = new NsdServiceInfo();
+        noHostInfo.setServiceName("kitten");
+        noHostInfo.setServiceType("_kitten._tcp");
+        noHostInfo.setPort(4242);
+        checkParcelable(noHostInfo);
+
+        NsdServiceInfo attributedInfo = new NsdServiceInfo();
+        attributedInfo.setServiceName("kitten");
+        attributedInfo.setServiceType("_kitten._tcp");
+        attributedInfo.setPort(4242);
+        attributedInfo.setHost(LOCALHOST);
+        attributedInfo.setAttribute("color", "pink");
+        attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
+        attributedInfo.setAttribute("adorable", (String) null);
+        attributedInfo.setAttribute("sticky", "yes");
+        attributedInfo.setAttribute("siblings", new byte[] {});
+        attributedInfo.setAttribute("edge cases", new byte[] {0, -1, 127, -128});
+        attributedInfo.removeAttribute("sticky");
+        checkParcelable(attributedInfo);
+
+        // Sanity check that we actually wrote attributes to attributedInfo.
+        assertTrue(attributedInfo.getAttributes().keySet().contains("adorable"));
+        String sound = new String(attributedInfo.getAttributes().get("sound"), "UTF-8");
+        assertTrue(sound.equals("にゃあ"));
+        byte[] edgeCases = attributedInfo.getAttributes().get("edge cases");
+        assertTrue(Arrays.equals(edgeCases, new byte[] {0, -1, 127, -128}));
+        assertFalse(attributedInfo.getAttributes().keySet().contains("sticky"));
+    }
+
+    public void checkParcelable(NsdServiceInfo original) {
+        // Write to parcel.
+        Parcel p = Parcel.obtain();
+        Bundle writer = new Bundle();
+        writer.putParcelable("test_info", original);
+        writer.writeToParcel(p, 0);
+
+        // Extract from parcel.
+        p.setDataPosition(0);
+        Bundle reader = p.readBundle();
+        reader.setClassLoader(NsdServiceInfo.class.getClassLoader());
+        NsdServiceInfo result = reader.getParcelable("test_info");
+
+        // Assert equality of base fields.
+        assertEquality(original.getServiceName(), result.getServiceName());
+        assertEquality(original.getServiceType(), result.getServiceType());
+        assertEquality(original.getHost(), result.getHost());
+        assertTrue(original.getPort() == result.getPort());
+
+        // Assert equality of attribute map.
+        Map<String, byte[]> originalMap = original.getAttributes();
+        Map<String, byte[]> resultMap = result.getAttributes();
+        assertEquality(originalMap.keySet(), resultMap.keySet());
+        for (String key : originalMap.keySet()) {
+            assertTrue(Arrays.equals(originalMap.get(key), resultMap.get(key)));
+        }
+    }
+
+    public void assertEquality(Object expected, Object result) {
+        assertTrue(expected == result || expected.equals(result));
+    }
+
+    public void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
+        assertTrue(null == shouldBeEmpty.getTxtRecord());
+    }
+}
diff --git a/tests/TEST_MAPPING b/tests/TEST_MAPPING
new file mode 100644
index 0000000..502f885
--- /dev/null
+++ b/tests/TEST_MAPPING
@@ -0,0 +1,34 @@
+{
+  "presubmit": [
+    {
+      "name": "FrameworksNetIntegrationTests"
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "FrameworksNetDeflakeTest"
+    }
+  ],
+  "auto-postsubmit": [
+    // Test tag for automotive targets. These are only running in postsubmit so as to harden the
+    // automotive targets to avoid introducing additional test flake and build time. The plan for
+    // presubmit testing for auto is to augment the existing tests to cover auto use cases as well.
+    // Additionally, this tag is used in targeted test suites to limit resource usage on the test
+    // infra during the hardening phase.
+    // TODO: this tag to be removed once the above is no longer an issue.
+    {
+      "name": "FrameworksNetTests"
+    },
+    {
+      "name": "FrameworksNetIntegrationTests"
+    },
+    {
+      "name": "FrameworksNetDeflakeTest"
+    }
+  ],
+  "imports": [
+    {
+      "path": "packages/modules/Connectivity"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
new file mode 100644
index 0000000..28339f1
--- /dev/null
+++ b/tests/common/Android.bp
@@ -0,0 +1,110 @@
+//
+// 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.
+//
+
+// Tests in this folder are included both in unit tests and CTS.
+// They must be fast and stable, and exercise public or test APIs.
+package {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "FrameworksNetCommonTests",
+    defaults: ["framework-connectivity-test-defaults"],
+    srcs: [
+        "java/**/*.java",
+        "java/**/*.kt",
+    ],
+    static_libs: [
+        "androidx.core_core",
+        "androidx.test.rules",
+        "junit",
+        "mockito-target-minus-junit4",
+        "modules-utils-build",
+        "net-tests-utils",
+        "net-utils-framework-common",
+        "platform-test-annotations",
+    ],
+    libs: [
+        "android.test.base.stubs",
+    ],
+}
+
+android_test {
+    name: "ConnectivityCoverageTests",
+    // Tethering started on SDK 30
+    min_sdk_version: "30",
+    // TODO: change to 31 as soon as it is available
+    target_sdk_version: "30",
+    test_suites: ["device-tests", "mts"],
+    defaults: [
+        "framework-connectivity-test-defaults",
+        "FrameworksNetTests-jni-defaults",
+        "libnetworkstackutilsjni_deps",
+    ],
+    manifest: "AndroidManifest_coverage.xml",
+    test_config: "AndroidTest_Coverage.xml",
+    static_libs: [
+        // Added first so all tests use extended mockito, instead of all tests using regular mockito
+        // (some tests would fail).
+        // TODO: consider removing extended mockito usage in tests that use it, for performance
+        "mockito-target-extended-minus-junit4",
+        "FrameworksNetTestsLib",
+        "modules-utils-native-coverage-listener",
+        "NetdStaticLibTestsLib",
+        "NetworkStaticLibTestsLib",
+        "NetworkStackTestsLib",
+        "TetheringTestsLatestSdkLib",
+        "TetheringIntegrationTestsLatestSdkLib",
+    ],
+    jni_libs: [
+        // For mockito extended
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+        // For NetworkStackUtils included in NetworkStackBase
+        "libnetworkstackutilsjni",
+        "libtetherutilsjni",
+        // For framework tests
+        "libservice-connectivity",
+    ],
+    libs: [
+        // Although not required to compile the static libs together, the "libs" used to build each
+        // of the common static test libs are necessary for R8 to avoid "Missing class" warnings and
+        // incorrect optimizations
+        "framework-tethering.impl",
+        "framework-wifi.stubs.module_lib",
+    ],
+    compile_multilib: "both",
+}
+
+// defaults for tests that need to build against framework-connectivity's @hide APIs
+// Only usable from targets that have visibility on framework-connectivity.impl.
+// Instead of using this, consider avoiding to depend on hidden connectivity APIs in
+// tests.
+java_defaults {
+    name: "framework-connectivity-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.impl",
+        "framework",
+
+        // if sdk_version="" this gets automatically included, but here we need to add manually.
+        "framework-res",
+    ],
+}
diff --git a/tests/common/AndroidManifest_coverage.xml b/tests/common/AndroidManifest_coverage.xml
new file mode 100644
index 0000000..8a22792
--- /dev/null
+++ b/tests/common/AndroidManifest_coverage.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="com.android.connectivity.tests.coverage">
+
+    <application tools:replace="android:label"
+                 android:debuggable="true"
+                 android:label="Connectivity coverage tests">
+        <uses-library android:name="android.test.runner" />
+    </application>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.connectivity.tests.coverage"
+                     android:label="Connectivity coverage tests">
+    </instrumentation>
+</manifest>
diff --git a/tests/common/AndroidTest_Coverage.xml b/tests/common/AndroidTest_Coverage.xml
new file mode 100644
index 0000000..577f36a
--- /dev/null
+++ b/tests/common/AndroidTest_Coverage.xml
@@ -0,0 +1,27 @@
+<!-- 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.
+-->
+<configuration description="Runs coverage tests for Connectivity">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="ConnectivityCoverageTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="ConnectivityCoverageTests" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.connectivity.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/tests/common/java/ParseExceptionTest.kt b/tests/common/java/ParseExceptionTest.kt
new file mode 100644
index 0000000..b702d61
--- /dev/null
+++ b/tests/common/java/ParseExceptionTest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+import android.net.ParseException
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ParseExceptionTest {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.R)
+
+    @Test
+    fun testConstructor_WithCause() {
+        val testMessage = "Test message"
+        val base = Exception("Test")
+        val exception = ParseException(testMessage, base)
+
+        assertEquals(testMessage, exception.response)
+        assertEquals(base, exception.cause)
+    }
+
+    @Test
+    fun testConstructor_NoCause() {
+        val testMessage = "Test message"
+        val exception = ParseException(testMessage)
+
+        assertEquals(testMessage, exception.response)
+        assertNull(exception.cause)
+    }
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/CaptivePortalDataTest.kt b/tests/common/java/android/net/CaptivePortalDataTest.kt
new file mode 100644
index 0000000..18a9331
--- /dev/null
+++ b/tests/common/java/android/net/CaptivePortalDataTest.kt
@@ -0,0 +1,190 @@
+/*
+ * 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
+
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.assertParcelSane
+import com.android.testutils.assertParcelingIsLossless
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.Q)
+class CaptivePortalDataTest {
+    @Rule @JvmField
+    val ignoreRule = DevSdkIgnoreRule()
+
+    private val data = CaptivePortalData.Builder()
+            .setRefreshTime(123L)
+            .setUserPortalUrl(Uri.parse("https://portal.example.com/test"))
+            .setVenueInfoUrl(Uri.parse("https://venue.example.com/test"))
+            .setSessionExtendable(true)
+            .setBytesRemaining(456L)
+            .setExpiryTime(789L)
+            .setCaptive(true)
+            .apply {
+                if (SdkLevel.isAtLeastS()) {
+                    setVenueFriendlyName("venue friendly name")
+                }
+            }
+            .build()
+
+    private val dataFromPasspoint = CaptivePortalData.Builder()
+            .setCaptive(true)
+            .apply {
+                if (SdkLevel.isAtLeastS()) {
+                    setVenueFriendlyName("venue friendly name")
+                    setUserPortalUrl(Uri.parse("https://tc.example.com/passpoint"),
+                            CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT)
+                    setVenueInfoUrl(Uri.parse("https://venue.example.com/passpoint"),
+                            CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT)
+                }
+            }
+            .build()
+
+    private fun makeBuilder() = CaptivePortalData.Builder(data)
+
+    @Test
+    fun testParcelUnparcel() {
+        val fieldCount = if (SdkLevel.isAtLeastS()) 10 else 7
+        assertParcelSane(data, fieldCount)
+        assertParcelSane(dataFromPasspoint, fieldCount)
+
+        assertParcelingIsLossless(makeBuilder().setUserPortalUrl(null).build())
+        assertParcelingIsLossless(makeBuilder().setVenueInfoUrl(null).build())
+    }
+
+    @Test
+    fun testEquals() {
+        assertEquals(data, makeBuilder().build())
+
+        assertNotEqualsAfterChange { it.setRefreshTime(456L) }
+        assertNotEqualsAfterChange { it.setUserPortalUrl(Uri.parse("https://example.com/")) }
+        assertNotEqualsAfterChange { it.setUserPortalUrl(null) }
+        assertNotEqualsAfterChange { it.setVenueInfoUrl(Uri.parse("https://example.com/")) }
+        assertNotEqualsAfterChange { it.setVenueInfoUrl(null) }
+        assertNotEqualsAfterChange { it.setSessionExtendable(false) }
+        assertNotEqualsAfterChange { it.setBytesRemaining(789L) }
+        assertNotEqualsAfterChange { it.setExpiryTime(12L) }
+        assertNotEqualsAfterChange { it.setCaptive(false) }
+
+        if (SdkLevel.isAtLeastS()) {
+            assertNotEqualsAfterChange { it.setVenueFriendlyName("another friendly name") }
+            assertNotEqualsAfterChange { it.setVenueFriendlyName(null) }
+
+            assertEquals(dataFromPasspoint, CaptivePortalData.Builder(dataFromPasspoint).build())
+            assertNotEqualsAfterChange { it.setUserPortalUrl(
+                    Uri.parse("https://tc.example.com/passpoint")) }
+            assertNotEqualsAfterChange { it.setUserPortalUrl(
+                    Uri.parse("https://tc.example.com/passpoint"),
+                    CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) }
+            assertNotEqualsAfterChange { it.setUserPortalUrl(
+                    Uri.parse("https://tc.example.com/other"),
+                    CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) }
+            assertNotEqualsAfterChange { it.setUserPortalUrl(
+                    Uri.parse("https://tc.example.com/passpoint"),
+                    CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) }
+            assertNotEqualsAfterChange { it.setVenueInfoUrl(
+                    Uri.parse("https://venue.example.com/passpoint")) }
+            assertNotEqualsAfterChange { it.setVenueInfoUrl(
+                    Uri.parse("https://venue.example.com/other"),
+                    CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) }
+            assertNotEqualsAfterChange { it.setVenueInfoUrl(
+                    Uri.parse("https://venue.example.com/passpoint"),
+                    CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) }
+        }
+    }
+
+    @Test
+    fun testUserPortalUrl() {
+        assertEquals(Uri.parse("https://portal.example.com/test"), data.userPortalUrl)
+    }
+
+    @Test
+    fun testVenueInfoUrl() {
+        assertEquals(Uri.parse("https://venue.example.com/test"), data.venueInfoUrl)
+    }
+
+    @Test
+    fun testIsSessionExtendable() {
+        assertTrue(data.isSessionExtendable)
+    }
+
+    @Test
+    fun testByteLimit() {
+        assertEquals(456L, data.byteLimit)
+        // Test byteLimit unset.
+        assertEquals(-1L, CaptivePortalData.Builder(null).build().byteLimit)
+    }
+
+    @Test
+    fun testRefreshTimeMillis() {
+        assertEquals(123L, data.refreshTimeMillis)
+    }
+
+    @Test
+    fun testExpiryTimeMillis() {
+        assertEquals(789L, data.expiryTimeMillis)
+        // Test expiryTimeMillis unset.
+        assertEquals(-1L, CaptivePortalData.Builder(null).build().expiryTimeMillis)
+    }
+
+    @Test
+    fun testIsCaptive() {
+        assertTrue(data.isCaptive)
+        assertFalse(makeBuilder().setCaptive(false).build().isCaptive)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    fun testVenueFriendlyName() {
+        assertEquals("venue friendly name", data.venueFriendlyName)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    fun testGetVenueInfoUrlSource() {
+        assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER,
+                data.venueInfoUrlSource)
+        assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT,
+                dataFromPasspoint.venueInfoUrlSource)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    fun testGetUserPortalUrlSource() {
+        assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER,
+                data.userPortalUrlSource)
+        assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT,
+                dataFromPasspoint.userPortalUrlSource)
+    }
+
+    private fun CaptivePortalData.mutate(mutator: (CaptivePortalData.Builder) -> Unit) =
+            CaptivePortalData.Builder(this).apply { mutator(this) }.build()
+
+    private fun assertNotEqualsAfterChange(mutator: (CaptivePortalData.Builder) -> Unit) {
+        assertNotEquals(data, data.mutate(mutator))
+    }
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/CaptivePortalTest.java b/tests/common/java/android/net/CaptivePortalTest.java
new file mode 100644
index 0000000..15d3398
--- /dev/null
+++ b/tests/common/java/android/net/CaptivePortalTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Build;
+import android.os.RemoteException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CaptivePortalTest {
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+    private static final int DEFAULT_TIMEOUT_MS = 5000;
+    private static final String TEST_PACKAGE_NAME = "com.google.android.test";
+
+    private final class MyCaptivePortalImpl extends ICaptivePortal.Stub {
+        int mCode = -1;
+        String mPackageName = null;
+
+        @Override
+        public void appResponse(final int response) throws RemoteException {
+            mCode = response;
+        }
+
+        @Override
+        public void appRequest(final int request) throws RemoteException {
+            mCode = request;
+        }
+
+        // This is only @Override on R-
+        public void logEvent(int eventId, String packageName) throws RemoteException {
+            mCode = eventId;
+            mPackageName = packageName;
+        }
+    }
+
+    private interface TestFunctor {
+        void useCaptivePortal(CaptivePortal o);
+    }
+
+    private MyCaptivePortalImpl runCaptivePortalTest(TestFunctor f) {
+        final MyCaptivePortalImpl cp = new MyCaptivePortalImpl();
+        f.useCaptivePortal(new CaptivePortal(cp.asBinder()));
+        return cp;
+    }
+
+    @Test
+    public void testReportCaptivePortalDismissed() {
+        final MyCaptivePortalImpl result =
+                runCaptivePortalTest(c -> c.reportCaptivePortalDismissed());
+        assertEquals(result.mCode, CaptivePortal.APP_RETURN_DISMISSED);
+    }
+
+    @Test
+    public void testIgnoreNetwork() {
+        final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.ignoreNetwork());
+        assertEquals(result.mCode, CaptivePortal.APP_RETURN_UNWANTED);
+    }
+
+    @Test
+    public void testUseNetwork() {
+        final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.useNetwork());
+        assertEquals(result.mCode, CaptivePortal.APP_RETURN_WANTED_AS_IS);
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
+    public void testReevaluateNetwork() {
+        final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.reevaluateNetwork());
+        assertEquals(result.mCode, CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED);
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testLogEvent() {
+        /**
+        * From S testLogEvent is expected to do nothing but shouldn't crash (the API
+        * logEvent has been deprecated).
+        */
+        final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.logEvent(
+                0,
+                TEST_PACKAGE_NAME));
+    }
+
+    @IgnoreAfter(Build.VERSION_CODES.R)
+    @Test
+    public void testLogEvent_UntilR() {
+        final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.logEvent(
+                42, TEST_PACKAGE_NAME));
+        assertEquals(result.mCode, 42);
+        assertEquals(result.mPackageName, TEST_PACKAGE_NAME);
+    }
+}
diff --git a/tests/common/java/android/net/DependenciesTest.java b/tests/common/java/android/net/DependenciesTest.java
new file mode 100644
index 0000000..ac1c28a
--- /dev/null
+++ b/tests/common/java/android/net/DependenciesTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A simple class that tests dependencies to java standard tools from the
+ * Network stack. These tests are not meant to be comprehensive tests of
+ * the relevant APIs : such tests belong in the relevant test suite for
+ * these dependencies. Instead, this just makes sure coverage is present
+ * by calling the methods in the exact way (or a representative way of how)
+ * they are called in the network stack.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DependenciesTest {
+    // Used to in ipmemorystore's RegularMaintenanceJobService to convert
+    // 24 hours into seconds
+    @Test
+    public void testTimeUnit() {
+        final int hours = 24;
+        final long inSeconds = TimeUnit.HOURS.toMillis(hours);
+        assertEquals(inSeconds, hours * 60 * 60 * 1000);
+    }
+
+    private byte[] makeTrivialArray(final int size) {
+        final byte[] src = new byte[size];
+        for (int i = 0; i < size; ++i) {
+            src[i] = (byte) i;
+        }
+        return src;
+    }
+
+    // Used in ApfFilter to find an IP address from a byte array
+    @Test
+    public void testArrays() {
+        final int size = 128;
+        final byte[] src = makeTrivialArray(size);
+
+        // Test copy
+        final int copySize = 16;
+        final int offset = 24;
+        final byte[] expected = new byte[copySize];
+        for (int i = 0; i < copySize; ++i) {
+            expected[i] = (byte) (offset + i);
+        }
+
+        final byte[] copy = Arrays.copyOfRange(src, offset, offset + copySize);
+        assertArrayEquals(expected, copy);
+        assertArrayEquals(new byte[0], Arrays.copyOfRange(src, size, size));
+    }
+
+    // Used mainly in the Dhcp code
+    @Test
+    public void testCopyOf() {
+        final byte[] src = makeTrivialArray(128);
+        final byte[] copy = Arrays.copyOf(src, src.length);
+        assertArrayEquals(src, copy);
+        assertFalse(src == copy);
+
+        assertArrayEquals(new byte[0], Arrays.copyOf(src, 0));
+
+        final int excess = 16;
+        final byte[] biggerCopy = Arrays.copyOf(src, src.length + excess);
+        for (int i = src.length; i < src.length + excess; ++i) {
+            assertEquals(0, biggerCopy[i]);
+        }
+        for (int i = src.length - 1; i >= 0; --i) {
+            assertEquals(src[i], biggerCopy[i]);
+        }
+    }
+
+    // Used mainly in DnsUtils but also various other places
+    @Test
+    public void testAsList() {
+        final int size = 24;
+        final Object[] src = new Object[size];
+        final ArrayList<Object> expected = new ArrayList<>(size);
+        for (int i = 0; i < size; ++i) {
+            final Object o = new Object();
+            src[i] = o;
+            expected.add(o);
+        }
+        assertEquals(expected, Arrays.asList(src));
+    }
+}
diff --git a/tests/common/java/android/net/DhcpInfoTest.java b/tests/common/java/android/net/DhcpInfoTest.java
new file mode 100644
index 0000000..ab4726b
--- /dev/null
+++ b/tests/common/java/android/net/DhcpInfoTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Inet4AddressUtils.inet4AddressToIntHTL;
+import static com.android.testutils.MiscAsserts.assertFieldCountEquals;
+import static com.android.testutils.ParcelUtils.parcelingRoundTrip;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.Nullable;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+@RunWith(AndroidJUnit4.class)
+public class DhcpInfoTest {
+    private static final String STR_ADDR1 = "255.255.255.255";
+    private static final String STR_ADDR2 = "127.0.0.1";
+    private static final String STR_ADDR3 = "192.168.1.1";
+    private static final String STR_ADDR4 = "192.168.1.0";
+    private static final int LEASE_TIME = 9999;
+
+    private int ipToInteger(String ipString) throws Exception {
+        return inet4AddressToIntHTL((Inet4Address) InetAddress.getByName(ipString));
+    }
+
+    private DhcpInfo createDhcpInfoObject() throws Exception {
+        final DhcpInfo dhcpInfo = new DhcpInfo();
+        dhcpInfo.ipAddress = ipToInteger(STR_ADDR1);
+        dhcpInfo.gateway = ipToInteger(STR_ADDR2);
+        dhcpInfo.netmask = ipToInteger(STR_ADDR3);
+        dhcpInfo.dns1 = ipToInteger(STR_ADDR4);
+        dhcpInfo.dns2 = ipToInteger(STR_ADDR4);
+        dhcpInfo.serverAddress = ipToInteger(STR_ADDR2);
+        dhcpInfo.leaseDuration = LEASE_TIME;
+        return dhcpInfo;
+    }
+
+    @Test
+    public void testConstructor() {
+        new DhcpInfo();
+    }
+
+    @Test
+    public void testToString() throws Exception {
+        final String expectedDefault = "ipaddr 0.0.0.0 gateway 0.0.0.0 netmask 0.0.0.0 "
+                + "dns1 0.0.0.0 dns2 0.0.0.0 DHCP server 0.0.0.0 lease 0 seconds";
+
+        DhcpInfo dhcpInfo = new DhcpInfo();
+
+        // Test default string.
+        assertEquals(expectedDefault, dhcpInfo.toString());
+
+        dhcpInfo = createDhcpInfoObject();
+
+        final String expected = "ipaddr " + STR_ADDR1 + " gateway " + STR_ADDR2 + " netmask "
+                + STR_ADDR3 + " dns1 " + STR_ADDR4 + " dns2 " + STR_ADDR4 + " DHCP server "
+                + STR_ADDR2 + " lease " + LEASE_TIME + " seconds";
+        // Test with new values
+        assertEquals(expected, dhcpInfo.toString());
+    }
+
+    private boolean dhcpInfoEquals(@Nullable DhcpInfo left, @Nullable DhcpInfo right) {
+        if (left == null && right == null) return true;
+
+        if (left == null || right == null) return false;
+
+        return left.ipAddress == right.ipAddress
+                && left.gateway == right.gateway
+                && left.netmask == right.netmask
+                && left.dns1 == right.dns1
+                && left.dns2 == right.dns2
+                && left.serverAddress == right.serverAddress
+                && left.leaseDuration == right.leaseDuration;
+    }
+
+    @Test
+    public void testParcelDhcpInfo() throws Exception {
+        // Cannot use assertParcelSane() here because this requires .equals() to work as
+        // defined, but DhcpInfo has a different legacy behavior that we cannot change.
+        final DhcpInfo dhcpInfo = createDhcpInfoObject();
+        assertFieldCountEquals(7, DhcpInfo.class);
+
+        final DhcpInfo dhcpInfoRoundTrip = parcelingRoundTrip(dhcpInfo);
+        assertTrue(dhcpInfoEquals(null, null));
+        assertFalse(dhcpInfoEquals(null, dhcpInfoRoundTrip));
+        assertFalse(dhcpInfoEquals(dhcpInfo, null));
+        assertTrue(dhcpInfoEquals(dhcpInfo, dhcpInfoRoundTrip));
+    }
+}
diff --git a/tests/common/java/android/net/IpPrefixTest.java b/tests/common/java/android/net/IpPrefixTest.java
new file mode 100644
index 0000000..50ecb42
--- /dev/null
+++ b/tests/common/java/android/net/IpPrefixTest.java
@@ -0,0 +1,374 @@
+/*
+ * 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 com.android.testutils.MiscAsserts.assertEqualBothWays;
+import static com.android.testutils.MiscAsserts.assertFieldCountEquals;
+import static com.android.testutils.MiscAsserts.assertNotEqualEitherWay;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+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.InetAddress;
+import java.util.Random;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpPrefixTest {
+
+    private static InetAddress address(String addr) {
+        return InetAddress.parseNumericAddress(addr);
+    }
+
+    // Explicitly cast everything to byte because "error: possible loss of precision".
+    private static final byte[] IPV4_BYTES = { (byte) 192, (byte) 0, (byte) 2, (byte) 4};
+    private static final byte[] IPV6_BYTES = {
+        (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+        (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef,
+        (byte) 0x0f, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xa0
+    };
+
+    @Test
+    public void testConstructor() {
+        IpPrefix p;
+        try {
+            p = new IpPrefix((byte[]) null, 9);
+            fail("Expected NullPointerException: null byte array");
+        } catch (RuntimeException expected) { }
+
+        try {
+            p = new IpPrefix((InetAddress) null, 10);
+            fail("Expected NullPointerException: null InetAddress");
+        } catch (RuntimeException expected) { }
+
+        try {
+            p = new IpPrefix((String) null);
+            fail("Expected NullPointerException: null String");
+        } catch (RuntimeException expected) { }
+
+
+        try {
+            byte[] b2 = {1, 2, 3, 4, 5};
+            p = new IpPrefix(b2, 29);
+            fail("Expected IllegalArgumentException: invalid array length");
+        } catch (IllegalArgumentException expected) { }
+
+        try {
+            p = new IpPrefix("1.2.3.4");
+            fail("Expected IllegalArgumentException: no prefix length");
+        } catch (IllegalArgumentException expected) { }
+
+        try {
+            p = new IpPrefix("1.2.3.4/");
+            fail("Expected IllegalArgumentException: empty prefix length");
+        } catch (IllegalArgumentException expected) { }
+
+        try {
+            p = new IpPrefix("foo/32");
+            fail("Expected IllegalArgumentException: invalid address");
+        } catch (IllegalArgumentException expected) { }
+
+        try {
+            p = new IpPrefix("1/32");
+            fail("Expected IllegalArgumentException: deprecated IPv4 format");
+        } catch (IllegalArgumentException expected) { }
+
+        try {
+            p = new IpPrefix("1.2.3.256/32");
+            fail("Expected IllegalArgumentException: invalid IPv4 address");
+        } catch (IllegalArgumentException expected) { }
+
+        try {
+            p = new IpPrefix("foo/32");
+            fail("Expected IllegalArgumentException: non-address");
+        } catch (IllegalArgumentException expected) { }
+
+        try {
+            p = new IpPrefix("f00:::/32");
+            fail("Expected IllegalArgumentException: invalid IPv6 address");
+        } catch (IllegalArgumentException expected) { }
+
+        p = new IpPrefix("/64");
+        assertEquals("::/64", p.toString());
+
+        p = new IpPrefix("/128");
+        assertEquals("::1/128", p.toString());
+
+        p = new IpPrefix("[2001:db8::123]/64");
+        assertEquals("2001:db8::/64", p.toString());
+    }
+
+    @Test
+    public void testTruncation() {
+        IpPrefix p;
+
+        p = new IpPrefix(IPV4_BYTES, 32);
+        assertEquals("192.0.2.4/32", p.toString());
+
+        p = new IpPrefix(IPV4_BYTES, 29);
+        assertEquals("192.0.2.0/29", p.toString());
+
+        p = new IpPrefix(IPV4_BYTES, 8);
+        assertEquals("192.0.0.0/8", p.toString());
+
+        p = new IpPrefix(IPV4_BYTES, 0);
+        assertEquals("0.0.0.0/0", p.toString());
+
+        try {
+            p = new IpPrefix(IPV4_BYTES, 33);
+            fail("Expected IllegalArgumentException: invalid prefix length");
+        } catch (RuntimeException expected) { }
+
+        try {
+            p = new IpPrefix(IPV4_BYTES, 128);
+            fail("Expected IllegalArgumentException: invalid prefix length");
+        } catch (RuntimeException expected) { }
+
+        try {
+            p = new IpPrefix(IPV4_BYTES, -1);
+            fail("Expected IllegalArgumentException: negative prefix length");
+        } catch (RuntimeException expected) { }
+
+        p = new IpPrefix(IPV6_BYTES, 128);
+        assertEquals("2001:db8:dead:beef:f00::a0/128", p.toString());
+
+        p = new IpPrefix(IPV6_BYTES, 122);
+        assertEquals("2001:db8:dead:beef:f00::80/122", p.toString());
+
+        p = new IpPrefix(IPV6_BYTES, 64);
+        assertEquals("2001:db8:dead:beef::/64", p.toString());
+
+        p = new IpPrefix(IPV6_BYTES, 3);
+        assertEquals("2000::/3", p.toString());
+
+        p = new IpPrefix(IPV6_BYTES, 0);
+        assertEquals("::/0", p.toString());
+
+        try {
+            p = new IpPrefix(IPV6_BYTES, -1);
+            fail("Expected IllegalArgumentException: negative prefix length");
+        } catch (RuntimeException expected) { }
+
+        try {
+            p = new IpPrefix(IPV6_BYTES, 129);
+            fail("Expected IllegalArgumentException: negative prefix length");
+        } catch (RuntimeException expected) { }
+
+    }
+
+    @Test
+    public void testEquals() {
+        IpPrefix p1, p2;
+
+        p1 = new IpPrefix("192.0.2.251/23");
+        p2 = new IpPrefix(new byte[]{(byte) 192, (byte) 0, (byte) 2, (byte) 251}, 23);
+        assertEqualBothWays(p1, p2);
+
+        p1 = new IpPrefix("192.0.2.5/23");
+        assertEqualBothWays(p1, p2);
+
+        p1 = new IpPrefix("192.0.2.5/24");
+        assertNotEqualEitherWay(p1, p2);
+
+        p1 = new IpPrefix("192.0.4.5/23");
+        assertNotEqualEitherWay(p1, p2);
+
+
+        p1 = new IpPrefix("2001:db8:dead:beef:f00::80/122");
+        p2 = new IpPrefix(IPV6_BYTES, 122);
+        assertEquals("2001:db8:dead:beef:f00::80/122", p2.toString());
+        assertEqualBothWays(p1, p2);
+
+        p1 = new IpPrefix("2001:db8:dead:beef:f00::bf/122");
+        assertEqualBothWays(p1, p2);
+
+        p1 = new IpPrefix("2001:db8:dead:beef:f00::8:0/123");
+        assertNotEqualEitherWay(p1, p2);
+
+        p1 = new IpPrefix("2001:db8:dead:beef::/122");
+        assertNotEqualEitherWay(p1, p2);
+
+        // 192.0.2.4/32 != c000:0204::/32.
+        byte[] ipv6bytes = new byte[16];
+        System.arraycopy(IPV4_BYTES, 0, ipv6bytes, 0, IPV4_BYTES.length);
+        p1 = new IpPrefix(ipv6bytes, 32);
+        assertEqualBothWays(p1, new IpPrefix("c000:0204::/32"));
+
+        p2 = new IpPrefix(IPV4_BYTES, 32);
+        assertNotEqualEitherWay(p1, p2);
+    }
+
+    @Test
+    public void testContainsInetAddress() {
+        IpPrefix p = new IpPrefix("2001:db8:f00::ace:d00d/127");
+        assertTrue(p.contains(address("2001:db8:f00::ace:d00c")));
+        assertTrue(p.contains(address("2001:db8:f00::ace:d00d")));
+        assertFalse(p.contains(address("2001:db8:f00::ace:d00e")));
+        assertFalse(p.contains(address("2001:db8:f00::bad:d00d")));
+        assertFalse(p.contains(address("2001:4868:4860::8888")));
+        assertFalse(p.contains(address("8.8.8.8")));
+
+        p = new IpPrefix("192.0.2.0/23");
+        assertTrue(p.contains(address("192.0.2.43")));
+        assertTrue(p.contains(address("192.0.3.21")));
+        assertFalse(p.contains(address("192.0.0.21")));
+        assertFalse(p.contains(address("8.8.8.8")));
+        assertFalse(p.contains(address("2001:4868:4860::8888")));
+
+        IpPrefix ipv6Default = new IpPrefix("::/0");
+        assertTrue(ipv6Default.contains(address("2001:db8::f00")));
+        assertFalse(ipv6Default.contains(address("192.0.2.1")));
+
+        IpPrefix ipv4Default = new IpPrefix("0.0.0.0/0");
+        assertTrue(ipv4Default.contains(address("255.255.255.255")));
+        assertTrue(ipv4Default.contains(address("192.0.2.1")));
+        assertFalse(ipv4Default.contains(address("2001:db8::f00")));
+    }
+
+    @Test
+    public void testContainsIpPrefix() {
+        assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("0.0.0.0/0")));
+        assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("1.2.3.4/0")));
+        assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("1.2.3.4/8")));
+        assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("1.2.3.4/24")));
+        assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("1.2.3.4/23")));
+
+        assertTrue(new IpPrefix("1.2.3.4/8").containsPrefix(new IpPrefix("1.2.3.4/8")));
+        assertTrue(new IpPrefix("1.2.3.4/8").containsPrefix(new IpPrefix("1.254.12.9/8")));
+        assertTrue(new IpPrefix("1.2.3.4/21").containsPrefix(new IpPrefix("1.2.3.4/21")));
+        assertTrue(new IpPrefix("1.2.3.4/32").containsPrefix(new IpPrefix("1.2.3.4/32")));
+
+        assertTrue(new IpPrefix("1.2.3.4/20").containsPrefix(new IpPrefix("1.2.3.0/24")));
+
+        assertFalse(new IpPrefix("1.2.3.4/32").containsPrefix(new IpPrefix("1.2.3.5/32")));
+        assertFalse(new IpPrefix("1.2.3.4/8").containsPrefix(new IpPrefix("2.2.3.4/8")));
+        assertFalse(new IpPrefix("0.0.0.0/16").containsPrefix(new IpPrefix("0.0.0.0/15")));
+        assertFalse(new IpPrefix("100.0.0.0/8").containsPrefix(new IpPrefix("99.0.0.0/8")));
+
+        assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("::/0")));
+        assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/1")));
+        assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("3d8a:661:a0::770/8")));
+        assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/8")));
+        assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/64")));
+        assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/113")));
+        assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/128")));
+
+        assertTrue(new IpPrefix("2001:db8:f00::ace:d00d/64").containsPrefix(
+                new IpPrefix("2001:db8:f00::ace:d00d/64")));
+        assertTrue(new IpPrefix("2001:db8:f00::ace:d00d/64").containsPrefix(
+                new IpPrefix("2001:db8:f00::ace:d00d/120")));
+        assertFalse(new IpPrefix("2001:db8:f00::ace:d00d/64").containsPrefix(
+                new IpPrefix("2001:db8:f00::ace:d00d/32")));
+        assertFalse(new IpPrefix("2001:db8:f00::ace:d00d/64").containsPrefix(
+                new IpPrefix("2006:db8:f00::ace:d00d/96")));
+
+        assertTrue(new IpPrefix("2001:db8:f00::ace:d00d/128").containsPrefix(
+                new IpPrefix("2001:db8:f00::ace:d00d/128")));
+        assertTrue(new IpPrefix("2001:db8:f00::ace:d00d/100").containsPrefix(
+                new IpPrefix("2001:db8:f00::ace:ccaf/110")));
+
+        assertFalse(new IpPrefix("2001:db8:f00::ace:d00d/128").containsPrefix(
+                new IpPrefix("2001:db8:f00::ace:d00e/128")));
+        assertFalse(new IpPrefix("::/30").containsPrefix(new IpPrefix("::/29")));
+    }
+
+    @Test
+    public void testHashCode() {
+        IpPrefix p = new IpPrefix(new byte[4], 0);
+        Random random = new Random();
+        for (int i = 0; i < 100; i++) {
+            final IpPrefix oldP = p;
+            if (random.nextBoolean()) {
+                // IPv4.
+                byte[] b = new byte[4];
+                random.nextBytes(b);
+                p = new IpPrefix(b, random.nextInt(33));
+            } else {
+                // IPv6.
+                byte[] b = new byte[16];
+                random.nextBytes(b);
+                p = new IpPrefix(b, random.nextInt(129));
+            }
+            if (p.equals(oldP)) {
+                assertEquals(p.hashCode(), oldP.hashCode());
+            }
+            if (p.hashCode() != oldP.hashCode()) {
+                assertNotEquals(p, oldP);
+            }
+        }
+    }
+
+    @Test
+    public void testHashCodeIsNotConstant() {
+        IpPrefix[] prefixes = {
+            new IpPrefix("2001:db8:f00::ace:d00d/127"),
+            new IpPrefix("192.0.2.0/23"),
+            new IpPrefix("::/0"),
+            new IpPrefix("0.0.0.0/0"),
+        };
+        for (int i = 0; i < prefixes.length; i++) {
+            for (int j = i + 1; j < prefixes.length; j++) {
+                assertNotEquals(prefixes[i].hashCode(), prefixes[j].hashCode());
+            }
+        }
+    }
+
+    @Test
+    public void testMappedAddressesAreBroken() {
+        // 192.0.2.0/24 != ::ffff:c000:0204/120, but because we use InetAddress,
+        // we are unable to comprehend that.
+        byte[] ipv6bytes = {
+            (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+            (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+            (byte) 0, (byte) 0, (byte) 0xff, (byte) 0xff,
+            (byte) 192, (byte) 0, (byte) 2, (byte) 0};
+        IpPrefix p = new IpPrefix(ipv6bytes, 120);
+        assertEquals(16, p.getRawAddress().length);       // Fine.
+        assertArrayEquals(ipv6bytes, p.getRawAddress());  // Fine.
+
+        // Broken.
+        assertEquals("192.0.2.0/120", p.toString());
+        assertEquals(InetAddress.parseNumericAddress("192.0.2.0"), p.getAddress());
+    }
+
+    @Test
+    public void testParceling() {
+        IpPrefix p;
+
+        p = new IpPrefix("2001:4860:db8::/64");
+        assertParcelingIsLossless(p);
+        assertTrue(p.isIPv6());
+
+        p = new IpPrefix("192.0.2.0/25");
+        assertParcelingIsLossless(p);
+        assertTrue(p.isIPv4());
+
+        assertFieldCountEquals(2, IpPrefix.class);
+    }
+}
diff --git a/tests/common/java/android/net/KeepalivePacketDataTest.kt b/tests/common/java/android/net/KeepalivePacketDataTest.kt
new file mode 100644
index 0000000..f464ec6
--- /dev/null
+++ b/tests/common/java/android/net/KeepalivePacketDataTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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
+
+import android.net.InvalidPacketException.ERROR_INVALID_IP_ADDRESS
+import android.net.InvalidPacketException.ERROR_INVALID_PORT
+import android.os.Build
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import java.net.InetAddress
+import java.util.Arrays
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class KeepalivePacketDataTest {
+    @Rule @JvmField
+    val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule()
+
+    private val INVALID_PORT = 65537
+    private val TEST_DST_PORT = 4244
+    private val TEST_SRC_PORT = 4243
+
+    private val TESTBYTES = byteArrayOf(12, 31, 22, 44)
+    private val TEST_SRC_ADDRV4 = "198.168.0.2".address()
+    private val TEST_DST_ADDRV4 = "198.168.0.1".address()
+    private val TEST_ADDRV6 = "2001:db8::1".address()
+
+    private fun String.address() = InetAddresses.parseNumericAddress(this)
+
+    // Add for test because constructor of KeepalivePacketData is protected.
+    private inner class TestKeepalivePacketData(
+        srcAddress: InetAddress? = TEST_SRC_ADDRV4,
+        srcPort: Int = TEST_SRC_PORT,
+        dstAddress: InetAddress? = TEST_DST_ADDRV4,
+        dstPort: Int = TEST_DST_PORT,
+        data: ByteArray = TESTBYTES
+    ) : KeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, data)
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testConstructor() {
+        var data: TestKeepalivePacketData
+
+        try {
+            data = TestKeepalivePacketData(srcAddress = null)
+            fail("Null src address should cause exception")
+        } catch (e: InvalidPacketException) {
+            assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
+        }
+
+        try {
+            data = TestKeepalivePacketData(dstAddress = null)
+            fail("Null dst address should cause exception")
+        } catch (e: InvalidPacketException) {
+            assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
+        }
+
+        try {
+            data = TestKeepalivePacketData(dstAddress = TEST_ADDRV6)
+            fail("Ip family mismatched should cause exception")
+        } catch (e: InvalidPacketException) {
+            assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
+        }
+
+        try {
+            data = TestKeepalivePacketData(srcPort = INVALID_PORT)
+            fail("Invalid srcPort should cause exception")
+        } catch (e: InvalidPacketException) {
+            assertEquals(e.error, ERROR_INVALID_PORT)
+        }
+
+        try {
+            data = TestKeepalivePacketData(dstPort = INVALID_PORT)
+            fail("Invalid dstPort should cause exception")
+        } catch (e: InvalidPacketException) {
+            assertEquals(e.error, ERROR_INVALID_PORT)
+        }
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testSrcAddress() = assertEquals(TEST_SRC_ADDRV4, TestKeepalivePacketData().srcAddress)
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testDstAddress() = assertEquals(TEST_DST_ADDRV4, TestKeepalivePacketData().dstAddress)
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testSrcPort() = assertEquals(TEST_SRC_PORT, TestKeepalivePacketData().srcPort)
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testDstPort() = assertEquals(TEST_DST_PORT, TestKeepalivePacketData().dstPort)
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testPacket() = assertTrue(Arrays.equals(TESTBYTES, TestKeepalivePacketData().packet))
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/LinkAddressTest.java b/tests/common/java/android/net/LinkAddressTest.java
new file mode 100644
index 0000000..2cf3cf9
--- /dev/null
+++ b/tests/common/java/android/net/LinkAddressTest.java
@@ -0,0 +1,518 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+import static android.system.OsConstants.IFA_F_DADFAILED;
+import static android.system.OsConstants.IFA_F_DEPRECATED;
+import static android.system.OsConstants.IFA_F_OPTIMISTIC;
+import static android.system.OsConstants.IFA_F_PERMANENT;
+import static android.system.OsConstants.IFA_F_TEMPORARY;
+import static android.system.OsConstants.IFA_F_TENTATIVE;
+import static android.system.OsConstants.RT_SCOPE_HOST;
+import static android.system.OsConstants.RT_SCOPE_LINK;
+import static android.system.OsConstants.RT_SCOPE_SITE;
+import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
+
+import static com.android.testutils.MiscAsserts.assertEqualBothWays;
+import static com.android.testutils.MiscAsserts.assertFieldCountEquals;
+import static com.android.testutils.MiscAsserts.assertNotEqualEitherWay;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.Build;
+import android.os.SystemClock;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+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.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class LinkAddressTest {
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+    private static final String V4 = "192.0.2.1";
+    private static final String V6 = "2001:db8::1";
+    private static final InetAddress V4_ADDRESS = InetAddresses.parseNumericAddress(V4);
+    private static final InetAddress V6_ADDRESS = InetAddresses.parseNumericAddress(V6);
+
+    @Test
+    public void testConstants() {
+        // RT_SCOPE_UNIVERSE = 0, but all the other constants should be nonzero.
+        assertNotEquals(0, RT_SCOPE_HOST);
+        assertNotEquals(0, RT_SCOPE_LINK);
+        assertNotEquals(0, RT_SCOPE_SITE);
+
+        assertNotEquals(0, IFA_F_DEPRECATED);
+        assertNotEquals(0, IFA_F_PERMANENT);
+        assertNotEquals(0, IFA_F_TENTATIVE);
+    }
+
+    @Test
+    public void testConstructors() throws SocketException {
+        LinkAddress address;
+
+        // Valid addresses work as expected.
+        address = new LinkAddress(V4_ADDRESS, 25);
+        assertEquals(V4_ADDRESS, address.getAddress());
+        assertEquals(25, address.getPrefixLength());
+        assertEquals(0, address.getFlags());
+        assertEquals(RT_SCOPE_UNIVERSE, address.getScope());
+        assertTrue(address.isIpv4());
+
+        address = new LinkAddress(V6_ADDRESS, 127);
+        assertEquals(V6_ADDRESS, address.getAddress());
+        assertEquals(127, address.getPrefixLength());
+        assertEquals(0, address.getFlags());
+        assertEquals(RT_SCOPE_UNIVERSE, address.getScope());
+        assertTrue(address.isIpv6());
+
+        // Nonsensical flags/scopes or combinations thereof are acceptable.
+        address = new LinkAddress(V6 + "/64", IFA_F_DEPRECATED | IFA_F_PERMANENT, RT_SCOPE_LINK);
+        assertEquals(V6_ADDRESS, address.getAddress());
+        assertEquals(64, address.getPrefixLength());
+        assertEquals(IFA_F_DEPRECATED | IFA_F_PERMANENT, address.getFlags());
+        assertEquals(RT_SCOPE_LINK, address.getScope());
+        assertTrue(address.isIpv6());
+
+        address = new LinkAddress(V4 + "/23", 123, 456);
+        assertEquals(V4_ADDRESS, address.getAddress());
+        assertEquals(23, address.getPrefixLength());
+        assertEquals(123, address.getFlags());
+        assertEquals(456, address.getScope());
+        assertTrue(address.isIpv4());
+
+        address = new LinkAddress("/64", 1 /* flags */, 2 /* scope */);
+        assertEquals(Inet6Address.LOOPBACK, address.getAddress());
+        assertEquals(64, address.getPrefixLength());
+        assertEquals(1, address.getFlags());
+        assertEquals(2, address.getScope());
+        assertTrue(address.isIpv6());
+
+        address = new LinkAddress("[2001:db8::123]/64", 3 /* flags */, 4 /* scope */);
+        assertEquals(InetAddresses.parseNumericAddress("2001:db8::123"), address.getAddress());
+        assertEquals(64, address.getPrefixLength());
+        assertEquals(3, address.getFlags());
+        assertEquals(4, address.getScope());
+        assertTrue(address.isIpv6());
+
+        // InterfaceAddress doesn't have a constructor. Fetch some from an interface.
+        List<InterfaceAddress> addrs = NetworkInterface.getByName("lo").getInterfaceAddresses();
+
+        // We expect to find 127.0.0.1/8 and ::1/128, in any order.
+        LinkAddress ipv4Loopback, ipv6Loopback;
+        assertEquals(2, addrs.size());
+        if (addrs.get(0).getAddress() instanceof Inet4Address) {
+            ipv4Loopback = new LinkAddress(addrs.get(0));
+            ipv6Loopback = new LinkAddress(addrs.get(1));
+        } else {
+            ipv4Loopback = new LinkAddress(addrs.get(1));
+            ipv6Loopback = new LinkAddress(addrs.get(0));
+        }
+
+        assertEquals(InetAddresses.parseNumericAddress("127.0.0.1"), ipv4Loopback.getAddress());
+        assertEquals(8, ipv4Loopback.getPrefixLength());
+
+        assertEquals(InetAddresses.parseNumericAddress("::1"), ipv6Loopback.getAddress());
+        assertEquals(128, ipv6Loopback.getPrefixLength());
+
+        // Null addresses are rejected.
+        try {
+            address = new LinkAddress(null, 24);
+            fail("Null InetAddress should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress((String) null, IFA_F_PERMANENT, RT_SCOPE_UNIVERSE);
+            fail("Null string should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress((InterfaceAddress) null);
+            fail("Null string should cause NullPointerException");
+        } catch(NullPointerException expected) {}
+
+        // Invalid prefix lengths are rejected.
+        try {
+            address = new LinkAddress(V4_ADDRESS, -1);
+            fail("Negative IPv4 prefix length should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress(V6_ADDRESS, -1);
+            fail("Negative IPv6 prefix length should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress(V4_ADDRESS, 33);
+            fail("/33 IPv4 prefix length should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress(V4 + "/33", IFA_F_PERMANENT, RT_SCOPE_UNIVERSE);
+            fail("/33 IPv4 prefix length should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+
+        try {
+            address = new LinkAddress(V6_ADDRESS, 129, IFA_F_PERMANENT, RT_SCOPE_UNIVERSE);
+            fail("/129 IPv6 prefix length should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress(V6 + "/129", IFA_F_PERMANENT, RT_SCOPE_UNIVERSE);
+            fail("/129 IPv6 prefix length should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        // Multicast addresses are rejected.
+        try {
+            address = new LinkAddress("224.0.0.2/32");
+            fail("IPv4 multicast address should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            address = new LinkAddress("ff02::1/128");
+            fail("IPv6 multicast address should cause IllegalArgumentException");
+        } catch(IllegalArgumentException expected) {}
+    }
+
+    @Test
+    public void testAddressScopes() {
+        assertEquals(RT_SCOPE_HOST, new LinkAddress("::/128").getScope());
+        assertEquals(RT_SCOPE_HOST, new LinkAddress("0.0.0.0/32").getScope());
+
+        assertEquals(RT_SCOPE_LINK, new LinkAddress("::1/128").getScope());
+        assertEquals(RT_SCOPE_LINK, new LinkAddress("127.0.0.5/8").getScope());
+        assertEquals(RT_SCOPE_LINK, new LinkAddress("fe80::ace:d00d/64").getScope());
+        assertEquals(RT_SCOPE_LINK, new LinkAddress("169.254.5.12/16").getScope());
+
+        assertEquals(RT_SCOPE_SITE, new LinkAddress("fec0::dead/64").getScope());
+
+        assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("10.1.2.3/21").getScope());
+        assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("192.0.2.1/25").getScope());
+        assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("2001:db8::/64").getScope());
+        assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("5000::/127").getScope());
+    }
+
+    private void assertIsSameAddressAs(LinkAddress l1, LinkAddress l2) {
+        assertTrue(l1 + " unexpectedly does not have same address as " + l2,
+                l1.isSameAddressAs(l2));
+        assertTrue(l2 + " unexpectedly does not have same address as " + l1,
+                l2.isSameAddressAs(l1));
+    }
+
+    private void assertIsNotSameAddressAs(LinkAddress l1, LinkAddress l2) {
+        assertFalse(l1 + " unexpectedly has same address as " + l2,
+                l1.isSameAddressAs(l2));
+        assertFalse(l2 + " unexpectedly has same address as " + l1,
+                l1.isSameAddressAs(l2));
+    }
+
+    @Test
+    public void testEqualsAndSameAddressAs() {
+        LinkAddress l1, l2, l3;
+
+        l1 = new LinkAddress("2001:db8::1/64");
+        l2 = new LinkAddress("2001:db8::1/64");
+        assertEqualBothWays(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+
+        l2 = new LinkAddress("2001:db8::1/65");
+        assertNotEqualEitherWay(l1, l2);
+        assertIsNotSameAddressAs(l1, l2);
+
+        l2 = new LinkAddress("2001:db8::2/64");
+        assertNotEqualEitherWay(l1, l2);
+        assertIsNotSameAddressAs(l1, l2);
+
+
+        l1 = new LinkAddress("192.0.2.1/24");
+        l2 = new LinkAddress("192.0.2.1/24");
+        assertEqualBothWays(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+
+        l2 = new LinkAddress("192.0.2.1/23");
+        assertNotEqualEitherWay(l1, l2);
+        assertIsNotSameAddressAs(l1, l2);
+
+        l2 = new LinkAddress("192.0.2.2/24");
+        assertNotEqualEitherWay(l1, l2);
+        assertIsNotSameAddressAs(l1, l2);
+
+
+        // Check equals() and isSameAddressAs() on identical addresses with different flags.
+        l1 = new LinkAddress(V6_ADDRESS, 64);
+        l2 = new LinkAddress(V6_ADDRESS, 64, 0, RT_SCOPE_UNIVERSE);
+        assertEqualBothWays(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+
+        l2 = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED, RT_SCOPE_UNIVERSE);
+        assertNotEqualEitherWay(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+
+        // Check equals() and isSameAddressAs() on identical addresses with different scope.
+        l1 = new LinkAddress(V4_ADDRESS, 24);
+        l2 = new LinkAddress(V4_ADDRESS, 24, 0, RT_SCOPE_UNIVERSE);
+        assertEqualBothWays(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+
+        l2 = new LinkAddress(V4_ADDRESS, 24, 0, RT_SCOPE_HOST);
+        assertNotEqualEitherWay(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+
+        // Addresses with the same start or end bytes aren't equal between families.
+        l1 = new LinkAddress("32.1.13.184/24");
+        l2 = new LinkAddress("2001:db8::1/24");
+        l3 = new LinkAddress("::2001:db8/24");
+
+        byte[] ipv4Bytes = l1.getAddress().getAddress();
+        byte[] l2FirstIPv6Bytes = Arrays.copyOf(l2.getAddress().getAddress(), 4);
+        byte[] l3LastIPv6Bytes = Arrays.copyOfRange(l3.getAddress().getAddress(), 12, 16);
+        assertTrue(Arrays.equals(ipv4Bytes, l2FirstIPv6Bytes));
+        assertTrue(Arrays.equals(ipv4Bytes, l3LastIPv6Bytes));
+
+        assertNotEqualEitherWay(l1, l2);
+        assertIsNotSameAddressAs(l1, l2);
+
+        assertNotEqualEitherWay(l1, l3);
+        assertIsNotSameAddressAs(l1, l3);
+
+        // Because we use InetAddress, an IPv4 address is equal to its IPv4-mapped address.
+        // TODO: Investigate fixing this.
+        String addressString = V4 + "/24";
+        l1 = new LinkAddress(addressString);
+        l2 = new LinkAddress("::ffff:" + addressString);
+        assertEqualBothWays(l1, l2);
+        assertIsSameAddressAs(l1, l2);
+    }
+
+    @Test
+    public void testHashCode() {
+        LinkAddress l1, l2;
+
+        l1 = new LinkAddress(V4_ADDRESS, 23);
+        l2 = new LinkAddress(V4_ADDRESS, 23, 0, RT_SCOPE_HOST);
+        assertNotEquals(l1.hashCode(), l2.hashCode());
+
+        l1 = new LinkAddress(V6_ADDRESS, 128);
+        l2 = new LinkAddress(V6_ADDRESS, 128, IFA_F_TENTATIVE, RT_SCOPE_UNIVERSE);
+        assertNotEquals(l1.hashCode(), l2.hashCode());
+    }
+
+    @Test
+    public void testParceling() {
+        LinkAddress l;
+
+        l = new LinkAddress(V6_ADDRESS, 64, 123, 456);
+        assertParcelingIsLossless(l);
+
+        l = new LinkAddress(V4 + "/28", IFA_F_PERMANENT, RT_SCOPE_LINK);
+        assertParcelingIsLossless(l);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testLifetimeParceling() {
+        final LinkAddress l = new LinkAddress(V6_ADDRESS, 64, 123, 456, 1L, 3600000L);
+        assertParcelingIsLossless(l);
+    }
+
+    @Test @IgnoreAfter(Build.VERSION_CODES.Q)
+    public void testFieldCount_Q() {
+        assertFieldCountEquals(4, LinkAddress.class);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testFieldCount() {
+        // Make sure any new field is covered by the above parceling tests when changing this number
+        assertFieldCountEquals(6, LinkAddress.class);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testDeprecationTime() {
+        try {
+            new LinkAddress(V6_ADDRESS, 64, 0, 456,
+                    LinkAddress.LIFETIME_UNKNOWN, 100000L);
+            fail("Only one time provided should cause exception");
+        } catch (IllegalArgumentException expected) { }
+
+        try {
+            new LinkAddress(V6_ADDRESS, 64, 0, 456,
+                    200000L, 100000L);
+            fail("deprecation time later than expiration time should cause exception");
+        } catch (IllegalArgumentException expected) { }
+
+        try {
+            new LinkAddress(V6_ADDRESS, 64, 0, 456,
+                    -2, 100000L);
+            fail("negative deprecation time should cause exception");
+        } catch (IllegalArgumentException expected) { }
+
+        LinkAddress addr = new LinkAddress(V6_ADDRESS, 64, 0, 456, 100000L, 200000L);
+        assertEquals(100000L, addr.getDeprecationTime());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testExpirationTime() {
+        try {
+            new LinkAddress(V6_ADDRESS, 64, 0, 456,
+                    200000L, LinkAddress.LIFETIME_UNKNOWN);
+            fail("Only one time provided should cause exception");
+        } catch (IllegalArgumentException expected) { }
+
+        try {
+            new LinkAddress(V6_ADDRESS, 64, 0, 456,
+                    100000L, -2);
+            fail("negative expiration time should cause exception");
+        } catch (IllegalArgumentException expected) { }
+
+        LinkAddress addr = new LinkAddress(V6_ADDRESS, 64, 0, 456, 100000L, 200000L);
+        assertEquals(200000L, addr.getExpirationTime());
+    }
+
+    @Test
+    public void testGetFlags() {
+        LinkAddress l = new LinkAddress(V6_ADDRESS, 64, 123, RT_SCOPE_HOST);
+        assertEquals(123, l.getFlags());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testGetFlags_Deprecation() {
+        // Test if deprecated bit was added/remove automatically based on the provided deprecation
+        // time
+        LinkAddress l = new LinkAddress(V6_ADDRESS, 64, 0, RT_SCOPE_HOST,
+                1L, LinkAddress.LIFETIME_PERMANENT);
+        // Check if the flag is added automatically.
+        assertTrue((l.getFlags() & IFA_F_DEPRECATED) != 0);
+
+        l = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED, RT_SCOPE_HOST,
+                SystemClock.elapsedRealtime() + 100000L, LinkAddress.LIFETIME_PERMANENT);
+        // Check if the flag is removed automatically.
+        assertTrue((l.getFlags() & IFA_F_DEPRECATED) == 0);
+
+        l = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED, RT_SCOPE_HOST,
+                LinkAddress.LIFETIME_PERMANENT, LinkAddress.LIFETIME_PERMANENT);
+        // Check if the permanent flag is added.
+        assertTrue((l.getFlags() & IFA_F_PERMANENT) != 0);
+
+        l = new LinkAddress(V6_ADDRESS, 64, IFA_F_PERMANENT, RT_SCOPE_HOST,
+                1000L, SystemClock.elapsedRealtime() + 100000L);
+        // Check if the permanent flag is removed
+        assertTrue((l.getFlags() & IFA_F_PERMANENT) == 0);
+    }
+
+    private void assertGlobalPreferred(LinkAddress l, String msg) {
+        assertTrue(msg, l.isGlobalPreferred());
+    }
+
+    private void assertNotGlobalPreferred(LinkAddress l, String msg) {
+        assertFalse(msg, l.isGlobalPreferred());
+    }
+
+    @Test
+    public void testIsGlobalPreferred() {
+        LinkAddress l;
+
+        l = new LinkAddress(V4_ADDRESS, 32, 0, RT_SCOPE_UNIVERSE);
+        assertGlobalPreferred(l, "v4,global,noflags");
+
+        l = new LinkAddress("10.10.1.7/23", 0, RT_SCOPE_UNIVERSE);
+        assertGlobalPreferred(l, "v4-rfc1918,global,noflags");
+
+        l = new LinkAddress("10.10.1.7/23", 0, RT_SCOPE_SITE);
+        assertNotGlobalPreferred(l, "v4-rfc1918,site-local,noflags");
+
+        l = new LinkAddress("127.0.0.7/8", 0, RT_SCOPE_HOST);
+        assertNotGlobalPreferred(l, "v4-localhost,node-local,noflags");
+
+        l = new LinkAddress(V6_ADDRESS, 64, 0, RT_SCOPE_UNIVERSE);
+        assertGlobalPreferred(l, "v6,global,noflags");
+
+        l = new LinkAddress(V6_ADDRESS, 64, IFA_F_PERMANENT, RT_SCOPE_UNIVERSE);
+        assertGlobalPreferred(l, "v6,global,permanent");
+
+        // IPv6 ULAs are not acceptable "global preferred" addresses.
+        l = new LinkAddress("fc12::1/64", 0, RT_SCOPE_UNIVERSE);
+        assertNotGlobalPreferred(l, "v6,ula1,noflags");
+
+        l = new LinkAddress("fd34::1/64", 0, RT_SCOPE_UNIVERSE);
+        assertNotGlobalPreferred(l, "v6,ula2,noflags");
+
+        l = new LinkAddress(V6_ADDRESS, 64, IFA_F_TEMPORARY, RT_SCOPE_UNIVERSE);
+        assertGlobalPreferred(l, "v6,global,tempaddr");
+
+        l = new LinkAddress(V6_ADDRESS, 64, (IFA_F_TEMPORARY|IFA_F_DADFAILED),
+                            RT_SCOPE_UNIVERSE);
+        assertNotGlobalPreferred(l, "v6,global,tempaddr+dadfailed");
+
+        l = new LinkAddress(V6_ADDRESS, 64, (IFA_F_TEMPORARY|IFA_F_DEPRECATED),
+                            RT_SCOPE_UNIVERSE);
+        assertNotGlobalPreferred(l, "v6,global,tempaddr+deprecated");
+
+        l = new LinkAddress(V6_ADDRESS, 64, IFA_F_TEMPORARY, RT_SCOPE_SITE);
+        assertNotGlobalPreferred(l, "v6,site-local,tempaddr");
+
+        l = new LinkAddress(V6_ADDRESS, 64, IFA_F_TEMPORARY, RT_SCOPE_LINK);
+        assertNotGlobalPreferred(l, "v6,link-local,tempaddr");
+
+        l = new LinkAddress(V6_ADDRESS, 64, IFA_F_TEMPORARY, RT_SCOPE_HOST);
+        assertNotGlobalPreferred(l, "v6,node-local,tempaddr");
+
+        l = new LinkAddress("::1/128", IFA_F_PERMANENT, RT_SCOPE_HOST);
+        assertNotGlobalPreferred(l, "v6-localhost,node-local,permanent");
+
+        l = new LinkAddress(V6_ADDRESS, 64, (IFA_F_TEMPORARY|IFA_F_TENTATIVE),
+                            RT_SCOPE_UNIVERSE);
+        assertNotGlobalPreferred(l, "v6,global,tempaddr+tentative");
+
+        l = new LinkAddress(V6_ADDRESS, 64,
+                            (IFA_F_TEMPORARY|IFA_F_TENTATIVE|IFA_F_OPTIMISTIC),
+                            RT_SCOPE_UNIVERSE);
+        assertGlobalPreferred(l, "v6,global,tempaddr+optimistic");
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testIsGlobalPreferred_DeprecatedInFuture() {
+        final LinkAddress l = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED,
+                RT_SCOPE_UNIVERSE, SystemClock.elapsedRealtime() + 100000,
+                SystemClock.elapsedRealtime() + 200000);
+        // Although the deprecated bit is set, but the deprecation time is in the future, test
+        // if the flag is removed automatically.
+        assertGlobalPreferred(l, "v6,global,tempaddr+deprecated in the future");
+    }
+}
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
new file mode 100644
index 0000000..550953d
--- /dev/null
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -0,0 +1,1271 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+import static android.net.RouteInfo.RTN_THROW;
+import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.RouteInfo.RTN_UNREACHABLE;
+
+import static com.android.testutils.ParcelUtils.assertParcelSane;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+import static com.android.testutils.ParcelUtils.parcelingRoundTrip;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.LinkProperties.ProvisioningChange;
+import android.os.Build;
+import android.system.OsConstants;
+import android.util.ArraySet;
+
+import androidx.core.os.BuildCompat;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+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.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class LinkPropertiesTest {
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+    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");
+    private static final InetAddress DNS2 = address("69.78.7.1");
+    private static final InetAddress DNS6 = address("2001:4860:4860::8888");
+    private static final InetAddress PRIVDNS1 = address("1.1.1.1");
+    private static final InetAddress PRIVDNS2 = address("1.0.0.1");
+    private static final InetAddress PRIVDNS6 = address("2606:4700:4700::1111");
+    private static final InetAddress PCSCFV4 = address("10.77.25.37");
+    private static final InetAddress PCSCFV6 = address("2001:0db8:85a3:0000:0000:8a2e:0370:1");
+    private static final InetAddress GATEWAY1 = address("75.208.8.1");
+    private static final InetAddress GATEWAY2 = address("69.78.8.1");
+    private static final InetAddress GATEWAY61 = address("fe80::6:0000:613");
+    private static final InetAddress GATEWAY62 = address("fe80::6:22%lo");
+    private static final InetAddress TESTIPV4ADDR = address("192.168.47.42");
+    private static final InetAddress TESTIPV6ADDR = address("fe80::7:33%43");
+    private static final Inet4Address DHCPSERVER = (Inet4Address) address("192.0.2.1");
+    private static final String NAME = "qmi0";
+    private static final String DOMAINS = "google.com";
+    private static final String PRIV_DNS_SERVER_NAME = "private.dns.com";
+    private static final String TCP_BUFFER_SIZES = "524288,1048576,2097152,262144,524288,1048576";
+    private static final int MTU = 1500;
+    private static final LinkAddress LINKADDRV4 = new LinkAddress(ADDRV4, 32);
+    private static final LinkAddress LINKADDRV6 = new LinkAddress(ADDRV6, 128);
+    private static final LinkAddress LINKADDRV6LINKLOCAL = new LinkAddress("fe80::1/64");
+    private static final Uri CAPPORT_API_URL = Uri.parse("https://test.example.com/capportapi");
+
+    // CaptivePortalData cannot be in a constant as it does not exist on Q.
+    // The test runner also crashes when scanning for tests if it is a return type.
+    private static Object getCaptivePortalData() {
+        return new CaptivePortalData.Builder()
+                .setVenueInfoUrl(Uri.parse("https://test.example.com/venue")).build();
+    }
+
+    private static InetAddress address(String addrString) {
+        return InetAddresses.parseNumericAddress(addrString);
+    }
+
+    private static boolean isAtLeastR() {
+        // BuildCompat.isAtLeastR is documented to return false on release SDKs (including R)
+        return Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || BuildCompat.isAtLeastR();
+    }
+
+    private void checkEmpty(final LinkProperties lp) {
+        assertEquals(0, lp.getAllInterfaceNames().size());
+        assertEquals(0, lp.getAllAddresses().size());
+        assertEquals(0, lp.getDnsServers().size());
+        assertEquals(0, lp.getValidatedPrivateDnsServers().size());
+        assertEquals(0, lp.getPcscfServers().size());
+        assertEquals(0, lp.getAllRoutes().size());
+        assertEquals(0, lp.getAllLinkAddresses().size());
+        assertEquals(0, lp.getStackedLinks().size());
+        assertEquals(0, lp.getMtu());
+        assertNull(lp.getPrivateDnsServerName());
+        assertNull(lp.getDomains());
+        assertNull(lp.getHttpProxy());
+        assertNull(lp.getTcpBufferSizes());
+        assertNull(lp.getNat64Prefix());
+        assertFalse(lp.isProvisioned());
+        assertFalse(lp.isIpv4Provisioned());
+        assertFalse(lp.isIpv6Provisioned());
+        assertFalse(lp.isPrivateDnsActive());
+
+        if (isAtLeastR()) {
+            assertNull(lp.getDhcpServerAddress());
+            assertFalse(lp.isWakeOnLanSupported());
+            assertNull(lp.getCaptivePortalApiUrl());
+            assertNull(lp.getCaptivePortalData());
+        }
+    }
+
+    private LinkProperties makeTestObject() {
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(NAME);
+        lp.addLinkAddress(LINKADDRV4);
+        lp.addLinkAddress(LINKADDRV6);
+        lp.addDnsServer(DNS1);
+        lp.addDnsServer(DNS2);
+        lp.addValidatedPrivateDnsServer(PRIVDNS1);
+        lp.addValidatedPrivateDnsServer(PRIVDNS2);
+        lp.setUsePrivateDns(true);
+        lp.setPrivateDnsServerName(PRIV_DNS_SERVER_NAME);
+        lp.addPcscfServer(PCSCFV6);
+        lp.setDomains(DOMAINS);
+        lp.addRoute(new RouteInfo(GATEWAY1));
+        lp.addRoute(new RouteInfo(GATEWAY2));
+        lp.setHttpProxy(ProxyInfo.buildDirectProxy("test", 8888));
+        lp.setMtu(MTU);
+        lp.setTcpBufferSizes(TCP_BUFFER_SIZES);
+        lp.setNat64Prefix(new IpPrefix("2001:db8:0:64::/96"));
+        if (isAtLeastR()) {
+            lp.setDhcpServerAddress(DHCPSERVER);
+            lp.setWakeOnLanSupported(true);
+            lp.setCaptivePortalApiUrl(CAPPORT_API_URL);
+            lp.setCaptivePortalData((CaptivePortalData) getCaptivePortalData());
+        }
+        return lp;
+    }
+
+    public void assertLinkPropertiesEqual(LinkProperties source, LinkProperties target) {
+        // Check implementation of equals(), element by element.
+        assertTrue(source.isIdenticalInterfaceName(target));
+        assertTrue(target.isIdenticalInterfaceName(source));
+
+        assertTrue(source.isIdenticalAddresses(target));
+        assertTrue(target.isIdenticalAddresses(source));
+
+        assertTrue(source.isIdenticalDnses(target));
+        assertTrue(target.isIdenticalDnses(source));
+
+        assertTrue(source.isIdenticalPrivateDns(target));
+        assertTrue(target.isIdenticalPrivateDns(source));
+
+        assertTrue(source.isIdenticalValidatedPrivateDnses(target));
+        assertTrue(target.isIdenticalValidatedPrivateDnses(source));
+
+        assertTrue(source.isIdenticalPcscfs(target));
+        assertTrue(target.isIdenticalPcscfs(source));
+
+        assertTrue(source.isIdenticalRoutes(target));
+        assertTrue(target.isIdenticalRoutes(source));
+
+        assertTrue(source.isIdenticalHttpProxy(target));
+        assertTrue(target.isIdenticalHttpProxy(source));
+
+        assertTrue(source.isIdenticalStackedLinks(target));
+        assertTrue(target.isIdenticalStackedLinks(source));
+
+        assertTrue(source.isIdenticalMtu(target));
+        assertTrue(target.isIdenticalMtu(source));
+
+        assertTrue(source.isIdenticalTcpBufferSizes(target));
+        assertTrue(target.isIdenticalTcpBufferSizes(source));
+
+        if (isAtLeastR()) {
+            assertTrue(source.isIdenticalDhcpServerAddress(target));
+            assertTrue(source.isIdenticalDhcpServerAddress(source));
+
+            assertTrue(source.isIdenticalWakeOnLan(target));
+            assertTrue(target.isIdenticalWakeOnLan(source));
+
+            assertTrue(source.isIdenticalCaptivePortalApiUrl(target));
+            assertTrue(target.isIdenticalCaptivePortalApiUrl(source));
+
+            assertTrue(source.isIdenticalCaptivePortalData(target));
+            assertTrue(target.isIdenticalCaptivePortalData(source));
+        }
+
+        // Check result of equals().
+        assertTrue(source.equals(target));
+        assertTrue(target.equals(source));
+
+        // Check hashCode.
+        assertEquals(source.hashCode(), target.hashCode());
+    }
+
+    @Test
+    public void testEqualsNull() {
+        LinkProperties source = new LinkProperties();
+        LinkProperties target = new LinkProperties();
+
+        assertFalse(source == target);
+        assertLinkPropertiesEqual(source, target);
+    }
+
+    @Test
+    public void testEqualsSameOrder() throws Exception {
+        LinkProperties source = new LinkProperties();
+        source.setInterfaceName(NAME);
+        // set 2 link addresses
+        source.addLinkAddress(LINKADDRV4);
+        source.addLinkAddress(LINKADDRV6);
+        // set 2 dnses
+        source.addDnsServer(DNS1);
+        source.addDnsServer(DNS2);
+        // set 1 pcscf
+        source.addPcscfServer(PCSCFV6);
+        // set 2 gateways
+        source.addRoute(new RouteInfo(GATEWAY1));
+        source.addRoute(new RouteInfo(GATEWAY2));
+        source.setMtu(MTU);
+
+        LinkProperties target = new LinkProperties();
+
+        // All fields are same
+        target.setInterfaceName(NAME);
+        target.addLinkAddress(LINKADDRV4);
+        target.addLinkAddress(LINKADDRV6);
+        target.addDnsServer(DNS1);
+        target.addDnsServer(DNS2);
+        target.addPcscfServer(PCSCFV6);
+        target.addRoute(new RouteInfo(GATEWAY1));
+        target.addRoute(new RouteInfo(GATEWAY2));
+        target.setMtu(MTU);
+
+        assertLinkPropertiesEqual(source, target);
+
+        target.clear();
+        // change Interface Name
+        target.setInterfaceName("qmi1");
+        target.addLinkAddress(LINKADDRV4);
+        target.addLinkAddress(LINKADDRV6);
+        target.addDnsServer(DNS1);
+        target.addDnsServer(DNS2);
+        target.addPcscfServer(PCSCFV6);
+        target.addRoute(new RouteInfo(GATEWAY1));
+        target.addRoute(new RouteInfo(GATEWAY2));
+        target.setMtu(MTU);
+        assertFalse(source.equals(target));
+
+        target.clear();
+        target.setInterfaceName(NAME);
+        // change link addresses
+        target.addLinkAddress(new LinkAddress(address("75.208.6.2"), 32));
+        target.addLinkAddress(LINKADDRV6);
+        target.addDnsServer(DNS1);
+        target.addDnsServer(DNS2);
+        target.addPcscfServer(PCSCFV6);
+        target.addRoute(new RouteInfo(GATEWAY1));
+        target.addRoute(new RouteInfo(GATEWAY2));
+        target.setMtu(MTU);
+        assertFalse(source.equals(target));
+
+        target.clear();
+        target.setInterfaceName(NAME);
+        target.addLinkAddress(LINKADDRV4);
+        target.addLinkAddress(LINKADDRV6);
+        // change dnses
+        target.addDnsServer(address("75.208.7.2"));
+        target.addDnsServer(DNS2);
+        target.addPcscfServer(PCSCFV6);
+        target.addRoute(new RouteInfo(GATEWAY1));
+        target.addRoute(new RouteInfo(GATEWAY2));
+        target.setMtu(MTU);
+        assertFalse(source.equals(target));
+
+        target.clear();
+        target.setInterfaceName(NAME);
+        target.addLinkAddress(LINKADDRV4);
+        target.addLinkAddress(LINKADDRV6);
+        target.addDnsServer(address("75.208.7.2"));
+        target.addDnsServer(DNS2);
+        // change pcscf
+        target.addPcscfServer(address("2001::1"));
+        target.addRoute(new RouteInfo(GATEWAY1));
+        target.addRoute(new RouteInfo(GATEWAY2));
+        target.setMtu(MTU);
+        assertFalse(source.equals(target));
+
+        target.clear();
+        target.setInterfaceName(NAME);
+        target.addLinkAddress(LINKADDRV4);
+        target.addLinkAddress(LINKADDRV6);
+        target.addDnsServer(DNS1);
+        target.addDnsServer(DNS2);
+        // change gateway
+        target.addRoute(new RouteInfo(address("75.208.8.2")));
+        target.setMtu(MTU);
+        target.addRoute(new RouteInfo(GATEWAY2));
+        assertFalse(source.equals(target));
+
+        target.clear();
+        target.setInterfaceName(NAME);
+        target.addLinkAddress(LINKADDRV4);
+        target.addLinkAddress(LINKADDRV6);
+        target.addDnsServer(DNS1);
+        target.addDnsServer(DNS2);
+        target.addRoute(new RouteInfo(GATEWAY1));
+        target.addRoute(new RouteInfo(GATEWAY2));
+        // change mtu
+        target.setMtu(1440);
+        assertFalse(source.equals(target));
+    }
+
+    @Test
+    public void testEqualsDifferentOrder() throws Exception {
+        LinkProperties source = new LinkProperties();
+        source.setInterfaceName(NAME);
+        // set 2 link addresses
+        source.addLinkAddress(LINKADDRV4);
+        source.addLinkAddress(LINKADDRV6);
+        // set 2 dnses
+        source.addDnsServer(DNS1);
+        source.addDnsServer(DNS2);
+        // set 2 gateways
+        source.addRoute(new RouteInfo(LINKADDRV4, GATEWAY1));
+        source.addRoute(new RouteInfo(GATEWAY2));
+        source.setMtu(MTU);
+
+        LinkProperties target = new LinkProperties();
+        // Exchange order
+        target.setInterfaceName(NAME);
+        target.addLinkAddress(LINKADDRV6);
+        target.addLinkAddress(LINKADDRV4);
+        target.addDnsServer(DNS2);
+        target.addDnsServer(DNS1);
+        target.addRoute(new RouteInfo(GATEWAY2));
+        target.addRoute(new RouteInfo(LINKADDRV4, GATEWAY1));
+        target.setMtu(MTU);
+
+        assertLinkPropertiesEqual(source, target);
+    }
+
+    @Test
+    public void testEqualsDuplicated() throws Exception {
+        LinkProperties source = new LinkProperties();
+        // set 3 link addresses, eg, [A, A, B]
+        source.addLinkAddress(LINKADDRV4);
+        source.addLinkAddress(LINKADDRV4);
+        source.addLinkAddress(LINKADDRV6);
+
+        LinkProperties target = new LinkProperties();
+        // set 3 link addresses, eg, [A, B, B]
+        target.addLinkAddress(LINKADDRV4);
+        target.addLinkAddress(LINKADDRV6);
+        target.addLinkAddress(LINKADDRV6);
+
+        assertLinkPropertiesEqual(source, target);
+    }
+
+    private void assertAllRoutesHaveInterface(String iface, LinkProperties lp) {
+        for (RouteInfo r : lp.getRoutes()) {
+            assertEquals(iface, r.getInterface());
+        }
+    }
+
+    private void assertAllRoutesNotHaveInterface(String iface, LinkProperties lp) {
+        for (RouteInfo r : lp.getRoutes()) {
+            assertNotEquals(iface, r.getInterface());
+        }
+    }
+
+    @Test
+    public void testRouteInterfaces() {
+        LinkAddress prefix1 = new LinkAddress(address("2001:db8:1::"), 48);
+        LinkAddress prefix2 = new LinkAddress(address("2001:db8:2::"), 48);
+        InetAddress address = ADDRV6;
+
+        // Add a route with no interface to a LinkProperties with no interface. No errors.
+        LinkProperties lp = new LinkProperties();
+        RouteInfo r = new RouteInfo(prefix1, address, null);
+        assertTrue(lp.addRoute(r));
+        assertEquals(1, lp.getRoutes().size());
+        assertAllRoutesHaveInterface(null, lp);
+
+        // Adding the same route twice has no effect.
+        assertFalse(lp.addRoute(r));
+        assertEquals(1, lp.getRoutes().size());
+
+        // Add a route with an interface. Expect an exception.
+        r = new RouteInfo(prefix2, address, "wlan0");
+        try {
+          lp.addRoute(r);
+          fail("Adding wlan0 route to LP with no interface, expect exception");
+        } catch (IllegalArgumentException expected) {}
+
+        // Change the interface name. All the routes should change their interface name too.
+        lp.setInterfaceName("rmnet0");
+        assertAllRoutesHaveInterface("rmnet0", lp);
+        assertAllRoutesNotHaveInterface(null, lp);
+        assertAllRoutesNotHaveInterface("wlan0", lp);
+
+        // Now add a route with the wrong interface. This causes an exception too.
+        try {
+          lp.addRoute(r);
+          fail("Adding wlan0 route to rmnet0 LP, expect exception");
+        } catch (IllegalArgumentException expected) {}
+
+        // If the interface name matches, the route is added.
+        r = new RouteInfo(prefix2, null, "wlan0");
+        lp.setInterfaceName("wlan0");
+        lp.addRoute(r);
+        assertEquals(2, lp.getRoutes().size());
+        assertAllRoutesHaveInterface("wlan0", lp);
+        assertAllRoutesNotHaveInterface("rmnet0", lp);
+
+        // Routes with null interfaces are converted to wlan0.
+        r = RouteInfo.makeHostRoute(ADDRV6, null);
+        lp.addRoute(r);
+        assertEquals(3, lp.getRoutes().size());
+        assertAllRoutesHaveInterface("wlan0", lp);
+
+        // Check routes are updated correctly when calling setInterfaceName.
+        LinkProperties lp2 = new LinkProperties(lp);
+        assertAllRoutesHaveInterface("wlan0", lp2);
+        final CompareResult<RouteInfo> cr1 =
+                new CompareResult<>(lp.getAllRoutes(), lp2.getAllRoutes());
+        assertEquals(0, cr1.added.size());
+        assertEquals(0, cr1.removed.size());
+
+        lp2.setInterfaceName("p2p0");
+        assertAllRoutesHaveInterface("p2p0", lp2);
+        assertAllRoutesNotHaveInterface("wlan0", lp2);
+        final CompareResult<RouteInfo> cr2 =
+                new CompareResult<>(lp.getAllRoutes(), lp2.getAllRoutes());
+        assertEquals(3, cr2.added.size());
+        assertEquals(3, cr2.removed.size());
+
+        // Remove route with incorrect interface, no route removed.
+        lp.removeRoute(new RouteInfo(prefix2, null, null));
+        assertEquals(3, lp.getRoutes().size());
+
+        // Check remove works when interface is correct.
+        lp.removeRoute(new RouteInfo(prefix2, null, "wlan0"));
+        assertEquals(2, lp.getRoutes().size());
+        assertAllRoutesHaveInterface("wlan0", lp);
+        assertAllRoutesNotHaveInterface("p2p0", lp);
+    }
+
+    @Test
+    public void testStackedInterfaces() {
+        LinkProperties rmnet0 = new LinkProperties();
+        rmnet0.setInterfaceName("rmnet0");
+        rmnet0.addLinkAddress(LINKADDRV6);
+
+        LinkProperties clat4 = new LinkProperties();
+        clat4.setInterfaceName("clat4");
+        clat4.addLinkAddress(LINKADDRV4);
+
+        assertEquals(0, rmnet0.getStackedLinks().size());
+        assertEquals(1, rmnet0.getAddresses().size());
+        assertEquals(1, rmnet0.getLinkAddresses().size());
+        assertEquals(1, rmnet0.getAllAddresses().size());
+        assertEquals(1, rmnet0.getAllLinkAddresses().size());
+        assertEquals(1, rmnet0.getAllInterfaceNames().size());
+        assertEquals("rmnet0", rmnet0.getAllInterfaceNames().get(0));
+
+        rmnet0.addStackedLink(clat4);
+        assertEquals(1, rmnet0.getStackedLinks().size());
+        assertEquals(1, rmnet0.getAddresses().size());
+        assertEquals(1, rmnet0.getLinkAddresses().size());
+        assertEquals(2, rmnet0.getAllAddresses().size());
+        assertEquals(2, rmnet0.getAllLinkAddresses().size());
+        assertEquals(2, rmnet0.getAllInterfaceNames().size());
+        assertEquals("rmnet0", rmnet0.getAllInterfaceNames().get(0));
+        assertEquals("clat4", rmnet0.getAllInterfaceNames().get(1));
+
+        rmnet0.addStackedLink(clat4);
+        assertEquals(1, rmnet0.getStackedLinks().size());
+        assertEquals(1, rmnet0.getAddresses().size());
+        assertEquals(1, rmnet0.getLinkAddresses().size());
+        assertEquals(2, rmnet0.getAllAddresses().size());
+        assertEquals(2, rmnet0.getAllLinkAddresses().size());
+        assertEquals(2, rmnet0.getAllInterfaceNames().size());
+        assertEquals("rmnet0", rmnet0.getAllInterfaceNames().get(0));
+        assertEquals("clat4", rmnet0.getAllInterfaceNames().get(1));
+
+        assertEquals(0, clat4.getStackedLinks().size());
+
+        // Modify an item in the returned collection to see what happens.
+        for (LinkProperties link : rmnet0.getStackedLinks()) {
+            if (link.getInterfaceName().equals("clat4")) {
+               link.setInterfaceName("newname");
+            }
+        }
+        for (LinkProperties link : rmnet0.getStackedLinks()) {
+            assertFalse("newname".equals(link.getInterfaceName()));
+        }
+
+        assertTrue(rmnet0.removeStackedLink("clat4"));
+        assertEquals(0, rmnet0.getStackedLinks().size());
+        assertEquals(1, rmnet0.getAddresses().size());
+        assertEquals(1, rmnet0.getLinkAddresses().size());
+        assertEquals(1, rmnet0.getAllAddresses().size());
+        assertEquals(1, rmnet0.getAllLinkAddresses().size());
+        assertEquals(1, rmnet0.getAllInterfaceNames().size());
+        assertEquals("rmnet0", rmnet0.getAllInterfaceNames().get(0));
+
+        assertFalse(rmnet0.removeStackedLink("clat4"));
+    }
+
+    private LinkAddress getFirstLinkAddress(LinkProperties lp) {
+        return lp.getLinkAddresses().iterator().next();
+    }
+
+    @Test
+    public void testAddressMethods() {
+        LinkProperties lp = new LinkProperties();
+
+        // No addresses.
+        assertFalse(lp.hasIpv4Address());
+        assertFalse(lp.hasGlobalIpv6Address());
+
+        // Addresses on stacked links don't count.
+        LinkProperties stacked = new LinkProperties();
+        stacked.setInterfaceName("stacked");
+        lp.addStackedLink(stacked);
+        stacked.addLinkAddress(LINKADDRV4);
+        stacked.addLinkAddress(LINKADDRV6);
+        assertTrue(stacked.hasIpv4Address());
+        assertTrue(stacked.hasGlobalIpv6Address());
+        assertFalse(lp.hasIpv4Address());
+        assertFalse(lp.hasGlobalIpv6Address());
+        lp.removeStackedLink("stacked");
+        assertFalse(lp.hasIpv4Address());
+        assertFalse(lp.hasGlobalIpv6Address());
+
+        // Addresses on the base link.
+        // Check the return values of hasIpvXAddress and ensure the add/remove methods return true
+        // iff something changes.
+        assertEquals(0, lp.getLinkAddresses().size());
+        assertTrue(lp.addLinkAddress(LINKADDRV6));
+        assertEquals(1, lp.getLinkAddresses().size());
+        assertFalse(lp.hasIpv4Address());
+        assertTrue(lp.hasGlobalIpv6Address());
+
+        assertTrue(lp.removeLinkAddress(LINKADDRV6));
+        assertEquals(0, lp.getLinkAddresses().size());
+
+        assertTrue(lp.addLinkAddress(LINKADDRV6LINKLOCAL));
+        assertEquals(1, lp.getLinkAddresses().size());
+        assertFalse(lp.hasGlobalIpv6Address());
+
+        assertTrue(lp.addLinkAddress(LINKADDRV4));
+        assertEquals(2, lp.getLinkAddresses().size());
+        assertTrue(lp.hasIpv4Address());
+        assertFalse(lp.hasGlobalIpv6Address());
+
+        assertTrue(lp.addLinkAddress(LINKADDRV6));
+        assertEquals(3, lp.getLinkAddresses().size());
+        assertTrue(lp.hasIpv4Address());
+        assertTrue(lp.hasGlobalIpv6Address());
+
+        assertTrue(lp.removeLinkAddress(LINKADDRV6LINKLOCAL));
+        assertEquals(2, lp.getLinkAddresses().size());
+        assertTrue(lp.hasIpv4Address());
+        assertTrue(lp.hasGlobalIpv6Address());
+
+        // Adding an address twice has no effect.
+        // Removing an address that's not present has no effect.
+        assertFalse(lp.addLinkAddress(LINKADDRV4));
+        assertEquals(2, lp.getLinkAddresses().size());
+        assertTrue(lp.hasIpv4Address());
+        assertTrue(lp.removeLinkAddress(LINKADDRV4));
+        assertEquals(1, lp.getLinkAddresses().size());
+        assertFalse(lp.hasIpv4Address());
+        assertFalse(lp.removeLinkAddress(LINKADDRV4));
+        assertEquals(1, lp.getLinkAddresses().size());
+
+        // Adding an address that's already present but with different properties causes the
+        // existing address to be updated and returns true.
+        // Start with only LINKADDRV6.
+        assertEquals(1, lp.getLinkAddresses().size());
+        assertEquals(LINKADDRV6, getFirstLinkAddress(lp));
+
+        // Create a LinkAddress object for the same address, but with different flags.
+        LinkAddress deprecated = new LinkAddress(ADDRV6, 128,
+                OsConstants.IFA_F_DEPRECATED, OsConstants.RT_SCOPE_UNIVERSE);
+        assertTrue(deprecated.isSameAddressAs(LINKADDRV6));
+        assertFalse(deprecated.equals(LINKADDRV6));
+
+        // Check that adding it updates the existing address instead of adding a new one.
+        assertTrue(lp.addLinkAddress(deprecated));
+        assertEquals(1, lp.getLinkAddresses().size());
+        assertEquals(deprecated, getFirstLinkAddress(lp));
+        assertFalse(LINKADDRV6.equals(getFirstLinkAddress(lp)));
+
+        // Removing LINKADDRV6 removes deprecated, because removing addresses ignores properties.
+        assertTrue(lp.removeLinkAddress(LINKADDRV6));
+        assertEquals(0, lp.getLinkAddresses().size());
+    }
+
+    @Test
+    public void testLinkAddresses() {
+        final LinkProperties lp = new LinkProperties();
+        lp.addLinkAddress(LINKADDRV4);
+        lp.addLinkAddress(LINKADDRV6);
+
+        final LinkProperties lp2 = new LinkProperties();
+        lp2.addLinkAddress(LINKADDRV6);
+
+        final LinkProperties lp3 = new LinkProperties();
+        final List<LinkAddress> linkAddresses = Arrays.asList(LINKADDRV4);
+        lp3.setLinkAddresses(linkAddresses);
+
+        assertFalse(lp.equals(lp2));
+        assertFalse(lp2.equals(lp3));
+
+        lp.removeLinkAddress(LINKADDRV4);
+        assertTrue(lp.equals(lp2));
+
+        lp2.setLinkAddresses(lp3.getLinkAddresses());
+        assertTrue(lp2.equals(lp3));
+    }
+
+    @Test
+    public void testNat64Prefix() throws Exception {
+        LinkProperties lp = new LinkProperties();
+        lp.addLinkAddress(LINKADDRV4);
+        lp.addLinkAddress(LINKADDRV6);
+
+        assertNull(lp.getNat64Prefix());
+
+        IpPrefix p = new IpPrefix("64:ff9b::/96");
+        lp.setNat64Prefix(p);
+        assertEquals(p, lp.getNat64Prefix());
+
+        p = new IpPrefix("2001:db8:a:b:1:2:3::/96");
+        lp.setNat64Prefix(p);
+        assertEquals(p, lp.getNat64Prefix());
+
+        p = new IpPrefix("2001:db8:a:b:1:2::/80");
+        try {
+            lp.setNat64Prefix(p);
+        } catch (IllegalArgumentException expected) {
+        }
+
+        p = new IpPrefix("64:ff9b::/64");
+        try {
+            lp.setNat64Prefix(p);
+        } catch (IllegalArgumentException expected) {
+        }
+
+        assertEquals(new IpPrefix("2001:db8:a:b:1:2:3::/96"), lp.getNat64Prefix());
+
+        lp.setNat64Prefix(null);
+        assertNull(lp.getNat64Prefix());
+    }
+
+    @Test
+    public void testIsProvisioned() {
+        LinkProperties lp4 = new LinkProperties();
+        assertFalse("v4only:empty", lp4.isProvisioned());
+        lp4.addLinkAddress(LINKADDRV4);
+        assertFalse("v4only:addr-only", lp4.isProvisioned());
+        lp4.addDnsServer(DNS1);
+        assertFalse("v4only:addr+dns", lp4.isProvisioned());
+        lp4.addRoute(new RouteInfo(GATEWAY1));
+        assertTrue("v4only:addr+dns+route", lp4.isProvisioned());
+        assertTrue("v4only:addr+dns+route", lp4.isIpv4Provisioned());
+        assertFalse("v4only:addr+dns+route", lp4.isIpv6Provisioned());
+
+        LinkProperties lp6 = new LinkProperties();
+        assertFalse("v6only:empty", lp6.isProvisioned());
+        lp6.addLinkAddress(LINKADDRV6LINKLOCAL);
+        assertFalse("v6only:fe80-only", lp6.isProvisioned());
+        lp6.addDnsServer(DNS6);
+        assertFalse("v6only:fe80+dns", lp6.isProvisioned());
+        lp6.addRoute(new RouteInfo(GATEWAY61));
+        assertFalse("v6only:fe80+dns+route", lp6.isProvisioned());
+        lp6.addLinkAddress(LINKADDRV6);
+        assertTrue("v6only:fe80+global+dns+route", lp6.isIpv6Provisioned());
+        assertTrue("v6only:fe80+global+dns+route", lp6.isProvisioned());
+        lp6.removeLinkAddress(LINKADDRV6LINKLOCAL);
+        assertFalse("v6only:global+dns+route", lp6.isIpv4Provisioned());
+        assertTrue("v6only:global+dns+route", lp6.isIpv6Provisioned());
+        assertTrue("v6only:global+dns+route", lp6.isProvisioned());
+
+        LinkProperties lp46 = new LinkProperties();
+        lp46.addLinkAddress(LINKADDRV4);
+        lp46.addLinkAddress(LINKADDRV6);
+        lp46.addDnsServer(DNS1);
+        lp46.addDnsServer(DNS6);
+        assertFalse("dualstack:missing-routes", lp46.isProvisioned());
+        lp46.addRoute(new RouteInfo(GATEWAY1));
+        assertTrue("dualstack:v4-provisioned", lp46.isIpv4Provisioned());
+        assertFalse("dualstack:v4-provisioned", lp46.isIpv6Provisioned());
+        assertTrue("dualstack:v4-provisioned", lp46.isProvisioned());
+        lp46.addRoute(new RouteInfo(GATEWAY61));
+        assertTrue("dualstack:both-provisioned", lp46.isIpv4Provisioned());
+        assertTrue("dualstack:both-provisioned", lp46.isIpv6Provisioned());
+        assertTrue("dualstack:both-provisioned", lp46.isProvisioned());
+
+        // A link with an IPv6 address and default route, but IPv4 DNS server.
+        LinkProperties mixed = new LinkProperties();
+        mixed.addLinkAddress(LINKADDRV6);
+        mixed.addDnsServer(DNS1);
+        mixed.addRoute(new RouteInfo(GATEWAY61));
+        assertFalse("mixed:addr6+route6+dns4", mixed.isIpv4Provisioned());
+        assertFalse("mixed:addr6+route6+dns4", mixed.isIpv6Provisioned());
+        assertFalse("mixed:addr6+route6+dns4", mixed.isProvisioned());
+    }
+
+    @Test
+    public void testCompareProvisioning() {
+        LinkProperties v4lp = new LinkProperties();
+        v4lp.addLinkAddress(LINKADDRV4);
+        v4lp.addRoute(new RouteInfo(GATEWAY1));
+        v4lp.addDnsServer(DNS1);
+        assertTrue(v4lp.isProvisioned());
+
+        LinkProperties v4r = new LinkProperties(v4lp);
+        v4r.removeDnsServer(DNS1);
+        assertFalse(v4r.isProvisioned());
+
+        assertEquals(ProvisioningChange.STILL_NOT_PROVISIONED,
+                LinkProperties.compareProvisioning(v4r, v4r));
+        assertEquals(ProvisioningChange.LOST_PROVISIONING,
+                LinkProperties.compareProvisioning(v4lp, v4r));
+        assertEquals(ProvisioningChange.GAINED_PROVISIONING,
+                LinkProperties.compareProvisioning(v4r, v4lp));
+        assertEquals(ProvisioningChange.STILL_PROVISIONED,
+                LinkProperties.compareProvisioning(v4lp, v4lp));
+
+        // Check that losing IPv4 provisioning on a dualstack network is
+        // seen as a total loss of provisioning.
+        LinkProperties v6lp = new LinkProperties();
+        v6lp.addLinkAddress(LINKADDRV6);
+        v6lp.addRoute(new RouteInfo(GATEWAY61));
+        v6lp.addDnsServer(DNS6);
+        assertFalse(v6lp.isIpv4Provisioned());
+        assertTrue(v6lp.isIpv6Provisioned());
+        assertTrue(v6lp.isProvisioned());
+
+        LinkProperties v46lp = new LinkProperties(v6lp);
+        v46lp.addLinkAddress(LINKADDRV4);
+        v46lp.addRoute(new RouteInfo(GATEWAY1));
+        v46lp.addDnsServer(DNS1);
+        assertTrue(v46lp.isIpv4Provisioned());
+        assertTrue(v46lp.isIpv6Provisioned());
+        assertTrue(v46lp.isProvisioned());
+
+        assertEquals(ProvisioningChange.STILL_PROVISIONED,
+                LinkProperties.compareProvisioning(v4lp, v46lp));
+        assertEquals(ProvisioningChange.STILL_PROVISIONED,
+                LinkProperties.compareProvisioning(v6lp, v46lp));
+        assertEquals(ProvisioningChange.LOST_PROVISIONING,
+                LinkProperties.compareProvisioning(v46lp, v6lp));
+        assertEquals(ProvisioningChange.LOST_PROVISIONING,
+                LinkProperties.compareProvisioning(v46lp, v4lp));
+
+        // Check that losing and gaining a secondary router does not change
+        // the provisioning status.
+        LinkProperties v6lp2 = new LinkProperties(v6lp);
+        v6lp2.addRoute(new RouteInfo(GATEWAY62));
+        assertTrue(v6lp2.isProvisioned());
+
+        assertEquals(ProvisioningChange.STILL_PROVISIONED,
+                LinkProperties.compareProvisioning(v6lp2, v6lp));
+        assertEquals(ProvisioningChange.STILL_PROVISIONED,
+                LinkProperties.compareProvisioning(v6lp, v6lp2));
+    }
+
+    @Test
+    public void testIsReachable() {
+        final LinkProperties v4lp = new LinkProperties();
+        assertFalse(v4lp.isReachable(DNS1));
+        assertFalse(v4lp.isReachable(DNS2));
+
+        // Add an on-link route, making the on-link DNS server reachable,
+        // but there is still no IPv4 address.
+        assertTrue(v4lp.addRoute(new RouteInfo(new IpPrefix(address("75.208.0.0"), 16))));
+        assertFalse(v4lp.isReachable(DNS1));
+        assertFalse(v4lp.isReachable(DNS2));
+
+        // Adding an IPv4 address (right now, any IPv4 address) means we use
+        // the routes to compute likely reachability.
+        assertTrue(v4lp.addLinkAddress(new LinkAddress(ADDRV4, 16)));
+        assertTrue(v4lp.isReachable(DNS1));
+        assertFalse(v4lp.isReachable(DNS2));
+
+        // Adding a default route makes the off-link DNS server reachable.
+        assertTrue(v4lp.addRoute(new RouteInfo(GATEWAY1)));
+        assertTrue(v4lp.isReachable(DNS1));
+        assertTrue(v4lp.isReachable(DNS2));
+
+        final LinkProperties v6lp = new LinkProperties();
+        final InetAddress kLinkLocalDns = address("fe80::6:1");
+        final InetAddress kLinkLocalDnsWithScope = address("fe80::6:2%43");
+        final InetAddress kOnLinkDns = address("2001:db8:85a3::53");
+        assertFalse(v6lp.isReachable(kLinkLocalDns));
+        assertFalse(v6lp.isReachable(kLinkLocalDnsWithScope));
+        assertFalse(v6lp.isReachable(kOnLinkDns));
+        assertFalse(v6lp.isReachable(DNS6));
+
+        // Add a link-local route, making the link-local DNS servers reachable. Because
+        // we assume the presence of an IPv6 link-local address, link-local DNS servers
+        // are considered reachable, but only those with a non-zero scope identifier.
+        assertTrue(v6lp.addRoute(new RouteInfo(new IpPrefix(address("fe80::"), 64))));
+        assertFalse(v6lp.isReachable(kLinkLocalDns));
+        assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope));
+        assertFalse(v6lp.isReachable(kOnLinkDns));
+        assertFalse(v6lp.isReachable(DNS6));
+
+        // Add a link-local address--nothing changes.
+        assertTrue(v6lp.addLinkAddress(LINKADDRV6LINKLOCAL));
+        assertFalse(v6lp.isReachable(kLinkLocalDns));
+        assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope));
+        assertFalse(v6lp.isReachable(kOnLinkDns));
+        assertFalse(v6lp.isReachable(DNS6));
+
+        // Add a global route on link, but no global address yet. DNS servers reachable
+        // via a route that doesn't require a gateway: give them the benefit of the
+        // doubt and hope the link-local source address suffices for communication.
+        assertTrue(v6lp.addRoute(new RouteInfo(new IpPrefix(address("2001:db8:85a3::"), 64))));
+        assertFalse(v6lp.isReachable(kLinkLocalDns));
+        assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope));
+        assertTrue(v6lp.isReachable(kOnLinkDns));
+        assertFalse(v6lp.isReachable(DNS6));
+
+        // Add a global address; the on-link global address DNS server is (still)
+        // presumed reachable.
+        assertTrue(v6lp.addLinkAddress(new LinkAddress(ADDRV6, 64)));
+        assertFalse(v6lp.isReachable(kLinkLocalDns));
+        assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope));
+        assertTrue(v6lp.isReachable(kOnLinkDns));
+        assertFalse(v6lp.isReachable(DNS6));
+
+        // Adding a default route makes the off-link DNS server reachable.
+        assertTrue(v6lp.addRoute(new RouteInfo(GATEWAY62)));
+        assertFalse(v6lp.isReachable(kLinkLocalDns));
+        assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope));
+        assertTrue(v6lp.isReachable(kOnLinkDns));
+        assertTrue(v6lp.isReachable(DNS6));
+
+        // Check isReachable on stacked links. This requires that the source IP address be assigned
+        // on the interface returned by the route lookup.
+        LinkProperties stacked = new LinkProperties();
+
+        // Can't add a stacked link without an interface name.
+        stacked.setInterfaceName("v4-test0");
+        v6lp.addStackedLink(stacked);
+
+        InetAddress stackedAddress = address("192.0.0.4");
+        LinkAddress stackedLinkAddress = new LinkAddress(stackedAddress, 32);
+        assertFalse(v6lp.isReachable(stackedAddress));
+        stacked.addLinkAddress(stackedLinkAddress);
+        assertFalse(v6lp.isReachable(stackedAddress));
+        stacked.addRoute(new RouteInfo(stackedLinkAddress));
+        assertTrue(stacked.isReachable(stackedAddress));
+        assertTrue(v6lp.isReachable(stackedAddress));
+
+        assertFalse(v6lp.isReachable(DNS1));
+        stacked.addRoute(new RouteInfo((IpPrefix) null, stackedAddress));
+        assertTrue(v6lp.isReachable(DNS1));
+    }
+
+    @Test
+    public void testLinkPropertiesEnsureDirectlyConnectedRoutes() {
+        // IPv4 case: no route added initially
+        LinkProperties rmnet0 = new LinkProperties();
+        rmnet0.setInterfaceName("rmnet0");
+        rmnet0.addLinkAddress(new LinkAddress("10.0.0.2/8"));
+        RouteInfo directRoute0 = new RouteInfo(new IpPrefix("10.0.0.0/8"), null,
+                rmnet0.getInterfaceName());
+
+        // Since no routes is added explicitly, getAllRoutes() should return empty.
+        assertTrue(rmnet0.getAllRoutes().isEmpty());
+        rmnet0.ensureDirectlyConnectedRoutes();
+        // ensureDirectlyConnectedRoutes() should have added the missing local route.
+        assertEqualRoutes(Collections.singletonList(directRoute0), rmnet0.getAllRoutes());
+
+        // IPv4 case: both direct and default routes added initially
+        LinkProperties rmnet1 = new LinkProperties();
+        rmnet1.setInterfaceName("rmnet1");
+        rmnet1.addLinkAddress(new LinkAddress("10.0.0.3/8"));
+        RouteInfo defaultRoute1 = new RouteInfo((IpPrefix) null, address("10.0.0.1"),
+                rmnet1.getInterfaceName());
+        RouteInfo directRoute1 = new RouteInfo(new IpPrefix("10.0.0.0/8"), null,
+                rmnet1.getInterfaceName());
+        rmnet1.addRoute(defaultRoute1);
+        rmnet1.addRoute(directRoute1);
+
+        // Check added routes
+        assertEqualRoutes(Arrays.asList(defaultRoute1, directRoute1), rmnet1.getAllRoutes());
+        // ensureDirectlyConnectedRoutes() shouldn't change the routes since direct connected
+        // route is already part of the configuration.
+        rmnet1.ensureDirectlyConnectedRoutes();
+        assertEqualRoutes(Arrays.asList(defaultRoute1, directRoute1), rmnet1.getAllRoutes());
+
+        // IPv6 case: only default routes added initially
+        LinkProperties rmnet2 = new LinkProperties();
+        rmnet2.setInterfaceName("rmnet2");
+        rmnet2.addLinkAddress(new LinkAddress("fe80::cafe/64"));
+        rmnet2.addLinkAddress(new LinkAddress("2001:db8::2/64"));
+        RouteInfo defaultRoute2 = new RouteInfo((IpPrefix) null, address("2001:db8::1"),
+                rmnet2.getInterfaceName());
+        RouteInfo directRoute2 = new RouteInfo(new IpPrefix("2001:db8::/64"), null,
+                rmnet2.getInterfaceName());
+        RouteInfo linkLocalRoute2 = new RouteInfo(new IpPrefix("fe80::/64"), null,
+                rmnet2.getInterfaceName());
+        rmnet2.addRoute(defaultRoute2);
+
+        assertEqualRoutes(Arrays.asList(defaultRoute2), rmnet2.getAllRoutes());
+        rmnet2.ensureDirectlyConnectedRoutes();
+        assertEqualRoutes(Arrays.asList(defaultRoute2, directRoute2, linkLocalRoute2),
+                rmnet2.getAllRoutes());
+
+        // Corner case: no interface name
+        LinkProperties rmnet3 = new LinkProperties();
+        rmnet3.addLinkAddress(new LinkAddress("192.168.0.2/24"));
+        RouteInfo directRoute3 = new RouteInfo(new IpPrefix("192.168.0.0/24"), null,
+                rmnet3.getInterfaceName());
+
+        assertTrue(rmnet3.getAllRoutes().isEmpty());
+        rmnet3.ensureDirectlyConnectedRoutes();
+        assertEqualRoutes(Collections.singletonList(directRoute3), rmnet3.getAllRoutes());
+    }
+
+    private void assertEqualRoutes(Collection<RouteInfo> expected, Collection<RouteInfo> actual) {
+        Set<RouteInfo> expectedSet = new ArraySet<>(expected);
+        Set<RouteInfo> actualSet = new ArraySet<>(actual);
+        // Duplicated entries in actual routes are considered failures
+        assertEquals(actual.size(), actualSet.size());
+
+        assertEquals(expectedSet, actualSet);
+    }
+
+    private static LinkProperties makeLinkPropertiesForParceling() {
+        LinkProperties source = new LinkProperties();
+        source.setInterfaceName(NAME);
+
+        source.addLinkAddress(LINKADDRV4);
+        source.addLinkAddress(LINKADDRV6);
+
+        source.addDnsServer(DNS1);
+        source.addDnsServer(DNS2);
+        source.addDnsServer(GATEWAY62);
+
+        source.addPcscfServer(TESTIPV4ADDR);
+        source.addPcscfServer(TESTIPV6ADDR);
+
+        source.setUsePrivateDns(true);
+        source.setPrivateDnsServerName(PRIV_DNS_SERVER_NAME);
+
+        source.setDomains(DOMAINS);
+
+        source.addRoute(new RouteInfo(GATEWAY1));
+        source.addRoute(new RouteInfo(GATEWAY2));
+
+        source.addValidatedPrivateDnsServer(DNS6);
+        source.addValidatedPrivateDnsServer(GATEWAY61);
+        source.addValidatedPrivateDnsServer(TESTIPV6ADDR);
+
+        source.setHttpProxy(ProxyInfo.buildDirectProxy("test", 8888));
+
+        source.setMtu(MTU);
+
+        source.setTcpBufferSizes(TCP_BUFFER_SIZES);
+
+        source.setNat64Prefix(new IpPrefix("2001:db8:1:2:64:64::/96"));
+
+        final LinkProperties stacked = new LinkProperties();
+        stacked.setInterfaceName("test-stacked");
+        source.addStackedLink(stacked);
+
+        return source;
+    }
+
+    @Test @IgnoreAfter(Build.VERSION_CODES.Q)
+    public void testLinkPropertiesParcelable_Q() throws Exception {
+        final LinkProperties source = makeLinkPropertiesForParceling();
+        assertParcelSane(source, 14 /* fieldCount */);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testLinkPropertiesParcelable() throws Exception {
+        final LinkProperties source = makeLinkPropertiesForParceling();
+
+        source.setWakeOnLanSupported(true);
+        source.setCaptivePortalApiUrl(CAPPORT_API_URL);
+        source.setCaptivePortalData((CaptivePortalData) getCaptivePortalData());
+        source.setDhcpServerAddress((Inet4Address) GATEWAY1);
+        assertParcelSane(new LinkProperties(source, true /* parcelSensitiveFields */),
+                18 /* fieldCount */);
+
+        // Verify that without using a sensitiveFieldsParcelingCopy, sensitive fields are cleared.
+        final LinkProperties sanitized = new LinkProperties(source);
+        sanitized.setCaptivePortalApiUrl(null);
+        sanitized.setCaptivePortalData(null);
+        assertEquals(sanitized, parcelingRoundTrip(source));
+    }
+
+    // Parceling of the scope was broken until Q-QPR2
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testLinkLocalDnsServerParceling() throws Exception {
+        final String strAddress = "fe80::1%lo";
+        final LinkProperties lp = new LinkProperties();
+        lp.addDnsServer(address(strAddress));
+        final LinkProperties unparceled = parcelingRoundTrip(lp);
+        // Inet6Address#equals does not test for the scope id
+        assertEquals(strAddress, unparceled.getDnsServers().get(0).getHostAddress());
+    }
+
+    @Test
+    public void testParcelUninitialized() throws Exception {
+        LinkProperties empty = new LinkProperties();
+        assertParcelingIsLossless(empty);
+    }
+
+    @Test
+    public void testConstructor() {
+        LinkProperties lp = new LinkProperties();
+        checkEmpty(lp);
+        assertLinkPropertiesEqual(lp, new LinkProperties(lp));
+        assertLinkPropertiesEqual(lp, new LinkProperties());
+
+        lp = makeTestObject();
+        assertLinkPropertiesEqual(lp, new LinkProperties(lp));
+    }
+
+    @Test
+    public void testDnsServers() {
+        final LinkProperties lp = new LinkProperties();
+        final List<InetAddress> dnsServers = Arrays.asList(DNS1, DNS2);
+        lp.setDnsServers(dnsServers);
+        assertEquals(2, lp.getDnsServers().size());
+        assertEquals(DNS1, lp.getDnsServers().get(0));
+        assertEquals(DNS2, lp.getDnsServers().get(1));
+
+        lp.removeDnsServer(DNS1);
+        assertEquals(1, lp.getDnsServers().size());
+        assertEquals(DNS2, lp.getDnsServers().get(0));
+
+        lp.addDnsServer(DNS6);
+        assertEquals(2, lp.getDnsServers().size());
+        assertEquals(DNS2, lp.getDnsServers().get(0));
+        assertEquals(DNS6, lp.getDnsServers().get(1));
+    }
+
+    @Test
+    public void testValidatedPrivateDnsServers() {
+        final LinkProperties lp = new LinkProperties();
+        final List<InetAddress> privDnsServers = Arrays.asList(PRIVDNS1, PRIVDNS2);
+        lp.setValidatedPrivateDnsServers(privDnsServers);
+        assertEquals(2, lp.getValidatedPrivateDnsServers().size());
+        assertEquals(PRIVDNS1, lp.getValidatedPrivateDnsServers().get(0));
+        assertEquals(PRIVDNS2, lp.getValidatedPrivateDnsServers().get(1));
+
+        lp.removeValidatedPrivateDnsServer(PRIVDNS1);
+        assertEquals(1, lp.getValidatedPrivateDnsServers().size());
+        assertEquals(PRIVDNS2, lp.getValidatedPrivateDnsServers().get(0));
+
+        lp.addValidatedPrivateDnsServer(PRIVDNS6);
+        assertEquals(2, lp.getValidatedPrivateDnsServers().size());
+        assertEquals(PRIVDNS2, lp.getValidatedPrivateDnsServers().get(0));
+        assertEquals(PRIVDNS6, lp.getValidatedPrivateDnsServers().get(1));
+    }
+
+    @Test
+    public void testPcscfServers() {
+        final LinkProperties lp = new LinkProperties();
+        final List<InetAddress> pcscfServers = Arrays.asList(PCSCFV4);
+        lp.setPcscfServers(pcscfServers);
+        assertEquals(1, lp.getPcscfServers().size());
+        assertEquals(PCSCFV4, lp.getPcscfServers().get(0));
+
+        lp.removePcscfServer(PCSCFV4);
+        assertEquals(0, lp.getPcscfServers().size());
+
+        lp.addPcscfServer(PCSCFV6);
+        assertEquals(1, lp.getPcscfServers().size());
+        assertEquals(PCSCFV6, lp.getPcscfServers().get(0));
+    }
+
+    @Test
+    public void testTcpBufferSizes() {
+        final LinkProperties lp = makeTestObject();
+        assertEquals(TCP_BUFFER_SIZES, lp.getTcpBufferSizes());
+
+        lp.setTcpBufferSizes(null);
+        assertNull(lp.getTcpBufferSizes());
+    }
+
+    @Test
+    public void testHasIpv6DefaultRoute() {
+        final LinkProperties lp = makeTestObject();
+        assertFalse(lp.hasIPv6DefaultRoute());
+
+        lp.addRoute(new RouteInfo(GATEWAY61));
+        assertTrue(lp.hasIPv6DefaultRoute());
+    }
+
+    @Test
+    public void testHttpProxy() {
+        final LinkProperties lp = makeTestObject();
+        assertTrue(lp.getHttpProxy().equals(ProxyInfo.buildDirectProxy("test", 8888)));
+    }
+
+    @Test
+    public void testPrivateDnsServerName() {
+        final LinkProperties lp = makeTestObject();
+        assertEquals(PRIV_DNS_SERVER_NAME, lp.getPrivateDnsServerName());
+
+        lp.setPrivateDnsServerName(null);
+        assertNull(lp.getPrivateDnsServerName());
+    }
+
+    @Test
+    public void testUsePrivateDns() {
+        final LinkProperties lp = makeTestObject();
+        assertTrue(lp.isPrivateDnsActive());
+
+        lp.clear();
+        assertFalse(lp.isPrivateDnsActive());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testDhcpServerAddress() {
+        final LinkProperties lp = makeTestObject();
+        assertEquals(DHCPSERVER, lp.getDhcpServerAddress());
+
+        lp.clear();
+        assertNull(lp.getDhcpServerAddress());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testWakeOnLanSupported() {
+        final LinkProperties lp = makeTestObject();
+        assertTrue(lp.isWakeOnLanSupported());
+
+        lp.clear();
+        assertFalse(lp.isWakeOnLanSupported());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testCaptivePortalApiUrl() {
+        final LinkProperties lp = makeTestObject();
+        assertEquals(CAPPORT_API_URL, lp.getCaptivePortalApiUrl());
+
+        lp.clear();
+        assertNull(lp.getCaptivePortalApiUrl());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testCaptivePortalData() {
+        final LinkProperties lp = makeTestObject();
+        assertEquals(getCaptivePortalData(), lp.getCaptivePortalData());
+
+        lp.clear();
+        assertNull(lp.getCaptivePortalData());
+    }
+
+    private LinkProperties makeIpv4LinkProperties() {
+        final LinkProperties linkProperties = new LinkProperties();
+        linkProperties.setInterfaceName(NAME);
+        linkProperties.addLinkAddress(LINKADDRV4);
+        linkProperties.addDnsServer(DNS1);
+        linkProperties.addRoute(new RouteInfo(GATEWAY1));
+        linkProperties.addRoute(new RouteInfo(GATEWAY2));
+        return linkProperties;
+    }
+
+    private LinkProperties makeIpv6LinkProperties() {
+        final LinkProperties linkProperties = new LinkProperties();
+        linkProperties.setInterfaceName(NAME);
+        linkProperties.addLinkAddress(LINKADDRV6);
+        linkProperties.addDnsServer(DNS6);
+        linkProperties.addRoute(new RouteInfo(GATEWAY61));
+        linkProperties.addRoute(new RouteInfo(GATEWAY62));
+        return linkProperties;
+    }
+
+    @Test
+    public void testHasIpv4DefaultRoute() {
+        final LinkProperties Ipv4 = makeIpv4LinkProperties();
+        assertTrue(Ipv4.hasIpv4DefaultRoute());
+        final LinkProperties Ipv6 = makeIpv6LinkProperties();
+        assertFalse(Ipv6.hasIpv4DefaultRoute());
+    }
+
+    @Test
+    public void testHasIpv4DnsServer() {
+        final LinkProperties Ipv4 = makeIpv4LinkProperties();
+        assertTrue(Ipv4.hasIpv4DnsServer());
+        final LinkProperties Ipv6 = makeIpv6LinkProperties();
+        assertFalse(Ipv6.hasIpv4DnsServer());
+    }
+
+    @Test
+    public void testHasIpv6DnsServer() {
+        final LinkProperties Ipv4 = makeIpv4LinkProperties();
+        assertFalse(Ipv4.hasIpv6DnsServer());
+        final LinkProperties Ipv6 = makeIpv6LinkProperties();
+        assertTrue(Ipv6.hasIpv6DnsServer());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testHasIpv4UnreachableDefaultRoute() {
+        final LinkProperties lp = makeTestObject();
+        assertFalse(lp.hasIpv4UnreachableDefaultRoute());
+        assertFalse(lp.hasIpv6UnreachableDefaultRoute());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE));
+        assertTrue(lp.hasIpv4UnreachableDefaultRoute());
+        assertFalse(lp.hasIpv6UnreachableDefaultRoute());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testHasIpv6UnreachableDefaultRoute() {
+        final LinkProperties lp = makeTestObject();
+        assertFalse(lp.hasIpv6UnreachableDefaultRoute());
+        assertFalse(lp.hasIpv4UnreachableDefaultRoute());
+
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE));
+        assertTrue(lp.hasIpv6UnreachableDefaultRoute());
+        assertFalse(lp.hasIpv4UnreachableDefaultRoute());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testRouteAddWithSameKey() throws Exception {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("wlan0");
+        final IpPrefix v6 = new IpPrefix("64:ff9b::/96");
+        lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1280));
+        assertEquals(1, lp.getRoutes().size());
+        lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1500));
+        assertEquals(1, lp.getRoutes().size());
+        final IpPrefix v4 = new IpPrefix("192.0.2.128/25");
+        lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_UNICAST, 1460));
+        assertEquals(2, lp.getRoutes().size());
+        lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_THROW, 1460));
+        assertEquals(2, lp.getRoutes().size());
+    }
+}
diff --git a/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt
new file mode 100644
index 0000000..a5e44d5
--- /dev/null
+++ b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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
+
+import android.net.wifi.aware.DiscoverySession
+import android.net.wifi.aware.PeerHandle
+import android.net.wifi.aware.WifiAwareNetworkSpecifier
+import android.os.Build
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+
+import com.android.testutils.assertParcelSane
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+
+import java.lang.IllegalStateException
+
+import org.junit.Assert.assertFalse
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class MatchAllNetworkSpecifierTest {
+    @Rule @JvmField
+    val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule()
+
+    private val specifier = MatchAllNetworkSpecifier()
+    private val discoverySession = Mockito.mock(DiscoverySession::class.java)
+    private val peerHandle = Mockito.mock(PeerHandle::class.java)
+    private val wifiAwareNetworkSpecifier = WifiAwareNetworkSpecifier.Builder(discoverySession,
+            peerHandle).build()
+
+    @Test
+    fun testParcel() {
+        assertParcelSane(MatchAllNetworkSpecifier(), 0)
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @IgnoreAfter(Build.VERSION_CODES.R)
+    // Only run this test on Android R.
+    // The method - satisfiedBy() has changed to canBeSatisfiedBy() starting from Android R, so the
+    // method - canBeSatisfiedBy() cannot be found when running this test on Android Q.
+    fun testCanBeSatisfiedBy_OnlyForR() {
+        // MatchAllNetworkSpecifier didn't follow its parent class to change the satisfiedBy() to
+        // canBeSatisfiedBy(), so if a caller calls MatchAllNetworkSpecifier#canBeSatisfiedBy(), the
+        // NetworkSpecifier#canBeSatisfiedBy() will be called actually, and false will be returned.
+        // Although it's not meeting the expectation, the behavior still needs to be verified.
+        assertFalse(specifier.canBeSatisfiedBy(wifiAwareNetworkSpecifier))
+    }
+
+    @Test(expected = IllegalStateException::class)
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    fun testCanBeSatisfiedBy() {
+        specifier.canBeSatisfiedBy(wifiAwareNetworkSpecifier)
+    }
+}
diff --git a/tests/common/java/android/net/NattKeepalivePacketDataTest.kt b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt
new file mode 100644
index 0000000..46f39dd
--- /dev/null
+++ b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt
@@ -0,0 +1,114 @@
+/*
+ * 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
+
+import android.net.InvalidPacketException.ERROR_INVALID_IP_ADDRESS
+import android.net.InvalidPacketException.ERROR_INVALID_PORT
+import android.net.NattSocketKeepalive.NATT_PORT
+import android.os.Build
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.assertEqualBothWays
+import com.android.testutils.assertFieldCountEquals
+import com.android.testutils.assertParcelSane
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.parcelingRoundTrip
+import java.net.InetAddress
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NattKeepalivePacketDataTest {
+    @Rule @JvmField
+    val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule()
+
+    /* Refer to the definition in {@code NattKeepalivePacketData} */
+    private val IPV4_HEADER_LENGTH = 20
+    private val UDP_HEADER_LENGTH = 8
+
+    private val TEST_PORT = 4243
+    private val TEST_PORT2 = 4244
+    private val TEST_SRC_ADDRV4 = "198.168.0.2".address()
+    private val TEST_DST_ADDRV4 = "198.168.0.1".address()
+    private val TEST_ADDRV6 = "2001:db8::1".address()
+
+    private fun String.address() = InetAddresses.parseNumericAddress(this)
+    private fun nattKeepalivePacket(
+        srcAddress: InetAddress? = TEST_SRC_ADDRV4,
+        srcPort: Int = TEST_PORT,
+        dstAddress: InetAddress? = TEST_DST_ADDRV4,
+        dstPort: Int = NATT_PORT
+    ) = NattKeepalivePacketData.nattKeepalivePacket(srcAddress, srcPort, dstAddress, dstPort)
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testConstructor() {
+        try {
+            nattKeepalivePacket(dstPort = TEST_PORT)
+            fail("Dst port is not NATT port should cause exception")
+        } catch (e: InvalidPacketException) {
+            assertEquals(e.error, ERROR_INVALID_PORT)
+        }
+
+        try {
+            nattKeepalivePacket(srcAddress = TEST_ADDRV6)
+            fail("A v6 srcAddress should cause exception")
+        } catch (e: InvalidPacketException) {
+            assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
+        }
+
+        try {
+            nattKeepalivePacket(dstAddress = TEST_ADDRV6)
+            fail("A v6 dstAddress should cause exception")
+        } catch (e: InvalidPacketException) {
+            assertEquals(e.error, ERROR_INVALID_IP_ADDRESS)
+        }
+
+        try {
+            parcelingRoundTrip(
+                    NattKeepalivePacketData(TEST_SRC_ADDRV4, TEST_PORT, TEST_DST_ADDRV4, TEST_PORT,
+                    byteArrayOf(12, 31, 22, 44)))
+            fail("Invalid data should cause exception")
+        } catch (e: IllegalArgumentException) { }
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testParcel() {
+        assertParcelSane(nattKeepalivePacket(), 0)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testEquals() {
+        assertEqualBothWays(nattKeepalivePacket(), nattKeepalivePacket())
+        assertNotEquals(nattKeepalivePacket(dstAddress = TEST_SRC_ADDRV4), nattKeepalivePacket())
+        assertNotEquals(nattKeepalivePacket(srcAddress = TEST_DST_ADDRV4), nattKeepalivePacket())
+        // Test src port only because dst port have to be NATT_PORT
+        assertNotEquals(nattKeepalivePacket(srcPort = TEST_PORT2), nattKeepalivePacket())
+        // Make sure the parceling test is updated if fields are added in the base class.
+        assertFieldCountEquals(5, KeepalivePacketData::class.java)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testHashCode() {
+        assertEquals(nattKeepalivePacket().hashCode(), nattKeepalivePacket().hashCode())
+    }
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt
new file mode 100644
index 0000000..afaae1c
--- /dev/null
+++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt
@@ -0,0 +1,94 @@
+/*
+ * 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
+
+import android.os.Build
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.assertParcelSane
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NetworkAgentConfigTest {
+    @Rule @JvmField
+    val ignoreRule = DevSdkIgnoreRule()
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testParcelNetworkAgentConfig() {
+        val config = NetworkAgentConfig.Builder().apply {
+            setExplicitlySelected(true)
+            setLegacyType(ConnectivityManager.TYPE_ETHERNET)
+            setSubscriberId("MySubId")
+            setPartialConnectivityAcceptable(false)
+            setUnvalidatedConnectivityAcceptable(true)
+            if (isAtLeastS()) {
+                setBypassableVpn(true)
+            }
+        }.build()
+        if (isAtLeastS()) {
+            // From S, the config will have 12 items
+            assertParcelSane(config, 12)
+        } else {
+            // For R or below, the config will have 10 items
+            assertParcelSane(config, 10)
+        }
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testBuilder() {
+        val testExtraInfo = "mylegacyExtraInfo"
+        val config = NetworkAgentConfig.Builder().apply {
+            setExplicitlySelected(true)
+            setLegacyType(ConnectivityManager.TYPE_ETHERNET)
+            setSubscriberId("MySubId")
+            setPartialConnectivityAcceptable(false)
+            setUnvalidatedConnectivityAcceptable(true)
+            setLegacyTypeName("TEST_NETWORK")
+            if (isAtLeastS()) {
+                setLegacyExtraInfo(testExtraInfo)
+                setNat64DetectionEnabled(false)
+                setProvisioningNotificationEnabled(false)
+                setBypassableVpn(true)
+            }
+        }.build()
+
+        assertTrue(config.isExplicitlySelected())
+        assertEquals(ConnectivityManager.TYPE_ETHERNET, config.getLegacyType())
+        assertEquals("MySubId", config.getSubscriberId())
+        assertFalse(config.isPartialConnectivityAcceptable())
+        assertTrue(config.isUnvalidatedConnectivityAcceptable())
+        assertEquals("TEST_NETWORK", config.getLegacyTypeName())
+        if (isAtLeastS()) {
+            assertEquals(testExtraInfo, config.getLegacyExtraInfo())
+            assertFalse(config.isNat64DetectionEnabled())
+            assertFalse(config.isProvisioningNotificationEnabled())
+            assertTrue(config.isBypassableVpn())
+        } else {
+            assertTrue(config.isNat64DetectionEnabled())
+            assertTrue(config.isProvisioningNotificationEnabled())
+        }
+    }
+}
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
new file mode 100644
index 0000000..a30d4f1
--- /dev/null
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -0,0 +1,1164 @@
+/*
+ * 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.NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED;
+import static android.net.NetworkCapabilities.MAX_TRANSPORT;
+import static android.net.NetworkCapabilities.MIN_TRANSPORT;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
+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;
+import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.os.Process.INVALID_UID;
+
+import static com.android.modules.utils.build.SdkLevel.isAtLeastR;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
+import static com.android.testutils.MiscAsserts.assertEmpty;
+import static com.android.testutils.MiscAsserts.assertThrows;
+import static com.android.testutils.ParcelUtils.assertParcelSane;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+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 android.net.wifi.aware.DiscoverySession;
+import android.net.wifi.aware.PeerHandle;
+import android.net.wifi.aware.WifiAwareNetworkSpecifier;
+import android.os.Build;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.ArraySet;
+import android.util.Range;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.CompatUtil;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkCapabilitiesTest {
+    private static final String TEST_SSID = "TEST_SSID";
+    private static final String DIFFERENT_TEST_SSID = "DIFFERENT_TEST_SSID";
+    private static final int TEST_SUBID1 = 1;
+    private static final int TEST_SUBID2 = 2;
+    private static final int TEST_SUBID3 = 3;
+
+    @Rule
+    public DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule();
+
+    private DiscoverySession mDiscoverySession = Mockito.mock(DiscoverySession.class);
+    private PeerHandle mPeerHandle = Mockito.mock(PeerHandle.class);
+
+    @Test
+    public void testMaybeMarkCapabilitiesRestricted() {
+        // check that internet does not get restricted
+        NetworkCapabilities netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_INTERNET);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        // metered-ness shouldn't matter
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_INTERNET);
+        netCap.addCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_INTERNET);
+        netCap.removeCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        // add EIMS - bundled with unrestricted means it's unrestricted
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_INTERNET);
+        netCap.addCapability(NET_CAPABILITY_EIMS);
+        netCap.addCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_INTERNET);
+        netCap.addCapability(NET_CAPABILITY_EIMS);
+        netCap.removeCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        // just a restricted cap should be restricted regardless of meteredness
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_EIMS);
+        netCap.addCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertFalse(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_EIMS);
+        netCap.removeCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertFalse(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        // try 2 restricted caps
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_CBS);
+        netCap.addCapability(NET_CAPABILITY_EIMS);
+        netCap.addCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertFalse(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        netCap = new NetworkCapabilities();
+        netCap.addCapability(NET_CAPABILITY_CBS);
+        netCap.addCapability(NET_CAPABILITY_EIMS);
+        netCap.removeCapability(NET_CAPABILITY_NOT_METERED);
+        netCap.maybeMarkCapabilitiesRestricted();
+        assertFalse(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+    }
+
+    @Test
+    public void testDescribeImmutableDifferences() {
+        NetworkCapabilities nc1;
+        NetworkCapabilities nc2;
+
+        // Transports changing
+        nc1 = new NetworkCapabilities().addTransportType(TRANSPORT_CELLULAR);
+        nc2 = new NetworkCapabilities().addTransportType(TRANSPORT_WIFI);
+        assertNotEquals("", nc1.describeImmutableDifferences(nc2));
+        assertEquals("", nc1.describeImmutableDifferences(nc1));
+
+        // Mutable capability changing
+        nc1 = new NetworkCapabilities().addCapability(NET_CAPABILITY_VALIDATED);
+        nc2 = new NetworkCapabilities();
+        assertEquals("", nc1.describeImmutableDifferences(nc2));
+        assertEquals("", nc1.describeImmutableDifferences(nc1));
+
+        // NOT_METERED changing (http://b/63326103)
+        nc1 = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_NOT_METERED)
+                .addCapability(NET_CAPABILITY_INTERNET);
+        nc2 = new NetworkCapabilities().addCapability(NET_CAPABILITY_INTERNET);
+        assertEquals("", nc1.describeImmutableDifferences(nc2));
+        assertEquals("", nc1.describeImmutableDifferences(nc1));
+
+        // Immutable capability changing
+        nc1 = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        nc2 = new NetworkCapabilities().addCapability(NET_CAPABILITY_INTERNET);
+        assertNotEquals("", nc1.describeImmutableDifferences(nc2));
+        assertEquals("", nc1.describeImmutableDifferences(nc1));
+
+        // Specifier changing
+        nc1 = new NetworkCapabilities().addTransportType(TRANSPORT_WIFI);
+        nc2 = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_WIFI)
+                .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier("eth42"));
+        assertNotEquals("", nc1.describeImmutableDifferences(nc2));
+        assertEquals("", nc1.describeImmutableDifferences(nc1));
+    }
+
+    @Test
+    public void testLinkBandwidthUtils() {
+        assertEquals(LINK_BANDWIDTH_UNSPECIFIED, NetworkCapabilities
+                .minBandwidth(LINK_BANDWIDTH_UNSPECIFIED, LINK_BANDWIDTH_UNSPECIFIED));
+        assertEquals(10, NetworkCapabilities
+                .minBandwidth(LINK_BANDWIDTH_UNSPECIFIED, 10));
+        assertEquals(10, NetworkCapabilities
+                .minBandwidth(10, LINK_BANDWIDTH_UNSPECIFIED));
+        assertEquals(10, NetworkCapabilities
+                .minBandwidth(10, 20));
+
+        assertEquals(LINK_BANDWIDTH_UNSPECIFIED, NetworkCapabilities
+                .maxBandwidth(LINK_BANDWIDTH_UNSPECIFIED, LINK_BANDWIDTH_UNSPECIFIED));
+        assertEquals(10, NetworkCapabilities
+                .maxBandwidth(LINK_BANDWIDTH_UNSPECIFIED, 10));
+        assertEquals(10, NetworkCapabilities
+                .maxBandwidth(10, LINK_BANDWIDTH_UNSPECIFIED));
+        assertEquals(20, NetworkCapabilities
+                .maxBandwidth(10, 20));
+    }
+
+    @Test
+    public void testSetUids() {
+        final NetworkCapabilities netCap = new NetworkCapabilities();
+        // Null uids match all UIDs
+        netCap.setUids(null);
+        assertTrue(netCap.appliesToUid(10));
+        assertTrue(netCap.appliesToUid(200));
+        assertTrue(netCap.appliesToUid(3000));
+        assertTrue(netCap.appliesToUid(10010));
+        assertTrue(netCap.appliesToUidRange(new UidRange(50, 100)));
+        assertTrue(netCap.appliesToUidRange(new UidRange(70, 72)));
+        assertTrue(netCap.appliesToUidRange(new UidRange(3500, 3912)));
+        assertTrue(netCap.appliesToUidRange(new UidRange(1, 100000)));
+
+        if (isAtLeastS()) {
+            final Set<Range<Integer>> uids = new ArraySet<>();
+            uids.add(uidRange(50, 100));
+            uids.add(uidRange(3000, 4000));
+            netCap.setUids(uids);
+            assertTrue(netCap.appliesToUid(50));
+            assertTrue(netCap.appliesToUid(80));
+            assertTrue(netCap.appliesToUid(100));
+            assertTrue(netCap.appliesToUid(3000));
+            assertTrue(netCap.appliesToUid(3001));
+            assertFalse(netCap.appliesToUid(10));
+            assertFalse(netCap.appliesToUid(25));
+            assertFalse(netCap.appliesToUid(49));
+            assertFalse(netCap.appliesToUid(101));
+            assertFalse(netCap.appliesToUid(2000));
+            assertFalse(netCap.appliesToUid(100000));
+
+            assertTrue(netCap.appliesToUidRange(new UidRange(50, 100)));
+            assertTrue(netCap.appliesToUidRange(new UidRange(70, 72)));
+            assertTrue(netCap.appliesToUidRange(new UidRange(3500, 3912)));
+            assertFalse(netCap.appliesToUidRange(new UidRange(1, 100)));
+            assertFalse(netCap.appliesToUidRange(new UidRange(49, 100)));
+            assertFalse(netCap.appliesToUidRange(new UidRange(1, 10)));
+            assertFalse(netCap.appliesToUidRange(new UidRange(60, 101)));
+            assertFalse(netCap.appliesToUidRange(new UidRange(60, 3400)));
+
+            NetworkCapabilities netCap2 = new NetworkCapabilities();
+            // A new netcap object has null UIDs, so anything will satisfy it.
+            assertTrue(netCap2.satisfiedByUids(netCap));
+            // Still not equal though.
+            assertFalse(netCap2.equalsUids(netCap));
+            netCap2.setUids(uids);
+            assertTrue(netCap2.satisfiedByUids(netCap));
+            assertTrue(netCap.equalsUids(netCap2));
+            assertTrue(netCap2.equalsUids(netCap));
+
+            uids.add(uidRange(600, 700));
+            netCap2.setUids(uids);
+            assertFalse(netCap2.satisfiedByUids(netCap));
+            assertFalse(netCap.appliesToUid(650));
+            assertTrue(netCap2.appliesToUid(650));
+            netCap.combineCapabilities(netCap2);
+            assertTrue(netCap2.satisfiedByUids(netCap));
+            assertTrue(netCap.appliesToUid(650));
+            assertFalse(netCap.appliesToUid(500));
+
+            assertTrue(new NetworkCapabilities().satisfiedByUids(netCap));
+            netCap.combineCapabilities(new NetworkCapabilities());
+            assertTrue(netCap.appliesToUid(500));
+            assertTrue(netCap.appliesToUidRange(new UidRange(1, 100000)));
+            assertFalse(netCap2.appliesToUid(500));
+            assertFalse(netCap2.appliesToUidRange(new UidRange(1, 100000)));
+            assertTrue(new NetworkCapabilities().satisfiedByUids(netCap));
+
+            // Null uids satisfies everything.
+            netCap.setUids(null);
+            assertTrue(netCap2.satisfiedByUids(netCap));
+            assertTrue(netCap.satisfiedByUids(netCap2));
+            netCap2.setUids(null);
+            assertTrue(netCap2.satisfiedByUids(netCap));
+            assertTrue(netCap.satisfiedByUids(netCap2));
+        }
+    }
+
+    @Test
+    public void testParcelNetworkCapabilities() {
+        final Set<Range<Integer>> uids = new ArraySet<>();
+        uids.add(uidRange(50, 100));
+        uids.add(uidRange(3000, 4000));
+        final NetworkCapabilities netCap = new NetworkCapabilities()
+            .addCapability(NET_CAPABILITY_INTERNET)
+            .addCapability(NET_CAPABILITY_EIMS)
+            .addCapability(NET_CAPABILITY_NOT_METERED);
+        if (isAtLeastS()) {
+            netCap.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2));
+            netCap.setUids(uids);
+        }
+        if (isAtLeastR()) {
+            netCap.setOwnerUid(123);
+            netCap.setAdministratorUids(new int[] {5, 11});
+        }
+        assertParcelingIsLossless(netCap);
+        netCap.setSSID(TEST_SSID);
+        testParcelSane(netCap);
+    }
+
+    @Test
+    public void testParcelNetworkCapabilitiesWithRequestorUidAndPackageName() {
+        final NetworkCapabilities netCap = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_EIMS)
+                .addCapability(NET_CAPABILITY_NOT_METERED);
+        if (isAtLeastR()) {
+            netCap.setRequestorPackageName("com.android.test");
+            netCap.setRequestorUid(9304);
+        }
+        assertParcelingIsLossless(netCap);
+        netCap.setSSID(TEST_SSID);
+        testParcelSane(netCap);
+    }
+
+    private void testParcelSane(NetworkCapabilities cap) {
+        if (isAtLeastS()) {
+            assertParcelSane(cap, 16);
+        } else if (isAtLeastR()) {
+            assertParcelSane(cap, 15);
+        } else {
+            assertParcelSane(cap, 11);
+        }
+    }
+
+    private static NetworkCapabilities createNetworkCapabilitiesWithTransportInfo() {
+        return new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_EIMS)
+                .addCapability(NET_CAPABILITY_NOT_METERED)
+                .setSSID(TEST_SSID)
+                .setTransportInfo(new TestTransportInfo())
+                .setRequestorPackageName("com.android.test")
+                .setRequestorUid(9304);
+    }
+
+    @Test
+    public void testNetworkCapabilitiesCopyWithNoRedactions() {
+        assumeTrue(isAtLeastS());
+
+        final NetworkCapabilities netCap = createNetworkCapabilitiesWithTransportInfo();
+        final NetworkCapabilities netCapWithNoRedactions =
+                new NetworkCapabilities(netCap, NetworkCapabilities.REDACT_NONE);
+        TestTransportInfo testTransportInfo =
+                (TestTransportInfo) netCapWithNoRedactions.getTransportInfo();
+        assertFalse(testTransportInfo.locationRedacted);
+        assertFalse(testTransportInfo.localMacAddressRedacted);
+        assertFalse(testTransportInfo.settingsRedacted);
+    }
+
+    @Test
+    public void testNetworkCapabilitiesCopyWithoutLocationSensitiveFields() {
+        assumeTrue(isAtLeastS());
+
+        final NetworkCapabilities netCap = createNetworkCapabilitiesWithTransportInfo();
+        final NetworkCapabilities netCapWithNoRedactions =
+                new NetworkCapabilities(netCap, REDACT_FOR_ACCESS_FINE_LOCATION);
+        TestTransportInfo testTransportInfo =
+                (TestTransportInfo) netCapWithNoRedactions.getTransportInfo();
+        assertTrue(testTransportInfo.locationRedacted);
+        assertFalse(testTransportInfo.localMacAddressRedacted);
+        assertFalse(testTransportInfo.settingsRedacted);
+    }
+
+    @Test
+    public void testOemPaid() {
+        NetworkCapabilities nc = new NetworkCapabilities();
+        // By default OEM_PAID is neither in the required or forbidden lists and the network is not
+        // restricted.
+        if (isAtLeastS()) {
+            assertFalse(nc.hasForbiddenCapability(NET_CAPABILITY_OEM_PAID));
+        }
+        assertFalse(nc.hasCapability(NET_CAPABILITY_OEM_PAID));
+        nc.maybeMarkCapabilitiesRestricted();
+        assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        // Adding OEM_PAID to capability list should make network restricted.
+        nc.addCapability(NET_CAPABILITY_OEM_PAID);
+        nc.addCapability(NET_CAPABILITY_INTERNET);  // Combine with unrestricted capability.
+        nc.maybeMarkCapabilitiesRestricted();
+        assertTrue(nc.hasCapability(NET_CAPABILITY_OEM_PAID));
+        assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        // Now let's make request for OEM_PAID network.
+        NetworkCapabilities nr = new NetworkCapabilities();
+        nr.addCapability(NET_CAPABILITY_OEM_PAID);
+        nr.maybeMarkCapabilitiesRestricted();
+        assertTrue(nr.satisfiedByNetworkCapabilities(nc));
+
+        // Request fails for network with the default capabilities.
+        assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities()));
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testOemPrivate() {
+        NetworkCapabilities nc = new NetworkCapabilities();
+        // By default OEM_PRIVATE is neither in the required or forbidden lists and the network is
+        // not restricted.
+        assertFalse(nc.hasForbiddenCapability(NET_CAPABILITY_OEM_PRIVATE));
+        assertFalse(nc.hasCapability(NET_CAPABILITY_OEM_PRIVATE));
+        nc.maybeMarkCapabilitiesRestricted();
+        assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        // Adding OEM_PRIVATE to capability list should make network restricted.
+        nc.addCapability(NET_CAPABILITY_OEM_PRIVATE);
+        nc.addCapability(NET_CAPABILITY_INTERNET);  // Combine with unrestricted capability.
+        nc.maybeMarkCapabilitiesRestricted();
+        assertTrue(nc.hasCapability(NET_CAPABILITY_OEM_PRIVATE));
+        assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        // Now let's make request for OEM_PRIVATE network.
+        NetworkCapabilities nr = new NetworkCapabilities();
+        nr.addCapability(NET_CAPABILITY_OEM_PRIVATE);
+        nr.maybeMarkCapabilitiesRestricted();
+        assertTrue(nr.satisfiedByNetworkCapabilities(nc));
+
+        // Request fails for network with the default capabilities.
+        assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities()));
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testForbiddenCapabilities() {
+        NetworkCapabilities network = new NetworkCapabilities();
+
+        NetworkCapabilities request = new NetworkCapabilities();
+        assertTrue("Request: " + request + ", Network:" + network,
+                request.satisfiedByNetworkCapabilities(network));
+
+        // Requesting absence of capabilities that network doesn't have. Request should satisfy.
+        request.addForbiddenCapability(NET_CAPABILITY_WIFI_P2P);
+        request.addForbiddenCapability(NET_CAPABILITY_NOT_METERED);
+        assertTrue(request.satisfiedByNetworkCapabilities(network));
+        assertArrayEquals(new int[]{NET_CAPABILITY_WIFI_P2P,
+                        NET_CAPABILITY_NOT_METERED},
+                request.getForbiddenCapabilities());
+
+        // This is a default capability, just want to make sure its there because we use it below.
+        assertTrue(network.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        // Verify that adding forbidden capability will effectively remove it from capability list.
+        request.addForbiddenCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        assertTrue(request.hasForbiddenCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        assertFalse(request.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        // Now this request won't be satisfied because network contains NOT_RESTRICTED.
+        assertFalse(request.satisfiedByNetworkCapabilities(network));
+        network.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        assertTrue(request.satisfiedByNetworkCapabilities(network));
+
+        // Verify that adding capability will effectively remove it from forbidden list
+        request.addCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        assertTrue(request.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        assertFalse(request.hasForbiddenCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+        assertFalse(request.satisfiedByNetworkCapabilities(network));
+        network.addCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        assertTrue(request.satisfiedByNetworkCapabilities(network));
+    }
+
+    @Test
+    public void testConnectivityManagedCapabilities() {
+        NetworkCapabilities nc = new NetworkCapabilities();
+        assertFalse(nc.hasConnectivityManagedCapability());
+        // Check every single system managed capability.
+        nc.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
+        assertTrue(nc.hasConnectivityManagedCapability());
+        nc.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
+        nc.addCapability(NET_CAPABILITY_FOREGROUND);
+        assertTrue(nc.hasConnectivityManagedCapability());
+        nc.removeCapability(NET_CAPABILITY_FOREGROUND);
+        nc.addCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY);
+        assertTrue(nc.hasConnectivityManagedCapability());
+        nc.removeCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY);
+        nc.addCapability(NET_CAPABILITY_VALIDATED);
+        assertTrue(nc.hasConnectivityManagedCapability());
+    }
+
+    @Test
+    public void testEqualsNetCapabilities() {
+        NetworkCapabilities nc1 = new NetworkCapabilities();
+        NetworkCapabilities nc2 = new NetworkCapabilities();
+        assertTrue(nc1.equalsNetCapabilities(nc2));
+        assertEquals(nc1, nc2);
+
+        nc1.addCapability(NET_CAPABILITY_MMS);
+        assertFalse(nc1.equalsNetCapabilities(nc2));
+        assertNotEquals(nc1, nc2);
+        nc2.addCapability(NET_CAPABILITY_MMS);
+        assertTrue(nc1.equalsNetCapabilities(nc2));
+        assertEquals(nc1, nc2);
+
+        if (isAtLeastS()) {
+            nc1.addForbiddenCapability(NET_CAPABILITY_INTERNET);
+            assertFalse(nc1.equalsNetCapabilities(nc2));
+            nc2.addForbiddenCapability(NET_CAPABILITY_INTERNET);
+            assertTrue(nc1.equalsNetCapabilities(nc2));
+
+            // Remove a required capability doesn't affect forbidden capabilities.
+            // This is a behaviour change from R to S.
+            nc1.removeCapability(NET_CAPABILITY_INTERNET);
+            assertTrue(nc1.equalsNetCapabilities(nc2));
+
+            nc1.removeForbiddenCapability(NET_CAPABILITY_INTERNET);
+            assertFalse(nc1.equalsNetCapabilities(nc2));
+            nc2.removeForbiddenCapability(NET_CAPABILITY_INTERNET);
+            assertTrue(nc1.equalsNetCapabilities(nc2));
+        }
+    }
+
+    @Test
+    public void testSSID() {
+        NetworkCapabilities nc1 = new NetworkCapabilities();
+        NetworkCapabilities nc2 = new NetworkCapabilities();
+        assertTrue(nc2.satisfiedBySSID(nc1));
+
+        nc1.setSSID(TEST_SSID);
+        assertTrue(nc2.satisfiedBySSID(nc1));
+        nc2.setSSID("different " + TEST_SSID);
+        assertFalse(nc2.satisfiedBySSID(nc1));
+
+        assertTrue(nc1.satisfiedByImmutableNetworkCapabilities(nc2));
+        assertFalse(nc1.satisfiedByNetworkCapabilities(nc2));
+    }
+
+    private ArraySet<Range<Integer>> uidRanges(int from, int to) {
+        final ArraySet<Range<Integer>> range = new ArraySet<>(1);
+        range.add(uidRange(from, to));
+        return range;
+    }
+
+    private Range<Integer> uidRange(int from, int to) {
+        return new Range<Integer>(from, to);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testSetAdministratorUids() {
+        NetworkCapabilities nc =
+                new NetworkCapabilities().setAdministratorUids(new int[] {2, 1, 3});
+
+        assertArrayEquals(new int[] {1, 2, 3}, nc.getAdministratorUids());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testSetAdministratorUidsWithDuplicates() {
+        try {
+            new NetworkCapabilities().setAdministratorUids(new int[] {1, 1});
+            fail("Expected IllegalArgumentException for duplicate uids");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testCombineCapabilities() {
+        NetworkCapabilities nc1 = new NetworkCapabilities();
+        NetworkCapabilities nc2 = new NetworkCapabilities();
+
+        if (isAtLeastS()) {
+            nc1.addForbiddenCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
+        }
+        nc1.addCapability(NET_CAPABILITY_NOT_ROAMING);
+        assertNotEquals(nc1, nc2);
+        nc2.combineCapabilities(nc1);
+        assertEquals(nc1, nc2);
+        assertTrue(nc2.hasCapability(NET_CAPABILITY_NOT_ROAMING));
+        if (isAtLeastS()) {
+            assertTrue(nc2.hasForbiddenCapability(NET_CAPABILITY_CAPTIVE_PORTAL));
+        }
+
+        if (isAtLeastS()) {
+            // This will effectively move NOT_ROAMING capability from required to forbidden for nc1.
+            nc1.addForbiddenCapability(NET_CAPABILITY_NOT_ROAMING);
+            // It is not allowed to have the same capability in both wanted and forbidden list.
+            assertThrows(IllegalArgumentException.class, () -> nc2.combineCapabilities(nc1));
+            // Remove forbidden capability to continue other tests.
+            nc1.removeForbiddenCapability(NET_CAPABILITY_NOT_ROAMING);
+        }
+
+        nc1.setSSID(TEST_SSID);
+        nc2.combineCapabilities(nc1);
+        if (isAtLeastR()) {
+            assertTrue(TEST_SSID.equals(nc2.getSsid()));
+        }
+
+        // Because they now have the same SSID, the following call should not throw
+        nc2.combineCapabilities(nc1);
+
+        nc1.setSSID(DIFFERENT_TEST_SSID);
+        try {
+            nc2.combineCapabilities(nc1);
+            fail("Expected IllegalStateException: can't combine different SSIDs");
+        } catch (IllegalStateException expected) {}
+        nc1.setSSID(TEST_SSID);
+
+        if (isAtLeastS()) {
+            nc1.setUids(uidRanges(10, 13));
+            assertNotEquals(nc1, nc2);
+            nc2.combineCapabilities(nc1);  // Everything + 10~13 is still everything.
+            assertNotEquals(nc1, nc2);
+            nc1.combineCapabilities(nc2);  // 10~13 + everything is everything.
+            assertEquals(nc1, nc2);
+            nc1.setUids(uidRanges(10, 13));
+            nc2.setUids(uidRanges(20, 23));
+            assertNotEquals(nc1, nc2);
+            nc1.combineCapabilities(nc2);
+            assertTrue(nc1.appliesToUid(12));
+            assertFalse(nc2.appliesToUid(12));
+            assertTrue(nc1.appliesToUid(22));
+            assertTrue(nc2.appliesToUid(22));
+
+            // Verify the subscription id list can be combined only when they are equal.
+            nc1.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2));
+            nc2.setSubscriptionIds(Set.of(TEST_SUBID2));
+            assertThrows(IllegalStateException.class, () -> nc2.combineCapabilities(nc1));
+
+            nc2.setSubscriptionIds(Set.of());
+            assertThrows(IllegalStateException.class, () -> nc2.combineCapabilities(nc1));
+
+            nc2.setSubscriptionIds(Set.of(TEST_SUBID2, TEST_SUBID1));
+            nc2.combineCapabilities(nc1);
+            assertEquals(Set.of(TEST_SUBID2, TEST_SUBID1), nc2.getSubscriptionIds());
+        }
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testCombineCapabilities_AdministratorUids() {
+        final NetworkCapabilities nc1 = new NetworkCapabilities();
+        final NetworkCapabilities nc2 = new NetworkCapabilities();
+
+        final int[] adminUids = {3, 6, 12};
+        nc1.setAdministratorUids(adminUids);
+        nc2.combineCapabilities(nc1);
+        assertTrue(nc2.equalsAdministratorUids(nc1));
+        assertArrayEquals(nc2.getAdministratorUids(), adminUids);
+
+        final int[] adminUidsOtherOrder = {3, 12, 6};
+        nc1.setAdministratorUids(adminUidsOtherOrder);
+        assertTrue(nc2.equalsAdministratorUids(nc1));
+
+        final int[] adminUids2 = {11, 1, 12, 3, 6};
+        nc1.setAdministratorUids(adminUids2);
+        assertFalse(nc2.equalsAdministratorUids(nc1));
+        assertFalse(Arrays.equals(nc2.getAdministratorUids(), adminUids2));
+        try {
+            nc2.combineCapabilities(nc1);
+            fail("Shouldn't be able to combine different lists of admin UIDs");
+        } catch (IllegalStateException expected) { }
+    }
+
+    @Test
+    public void testSetCapabilities() {
+        final int[] REQUIRED_CAPABILITIES = new int[] {
+                NET_CAPABILITY_INTERNET, NET_CAPABILITY_NOT_VPN };
+
+        NetworkCapabilities nc1 = new NetworkCapabilities();
+        NetworkCapabilities nc2 = new NetworkCapabilities();
+
+        nc1.setCapabilities(REQUIRED_CAPABILITIES);
+        assertArrayEquals(REQUIRED_CAPABILITIES, nc1.getCapabilities());
+
+        // Verify that setting and adding capabilities leads to the same object state.
+        nc2.clearAll();
+        for (int cap : REQUIRED_CAPABILITIES) {
+            nc2.addCapability(cap);
+        }
+        assertEquals(nc1, nc2);
+
+        if (isAtLeastS()) {
+            final int[] forbiddenCapabilities = new int[]{
+                    NET_CAPABILITY_NOT_METERED, NET_CAPABILITY_NOT_RESTRICTED };
+
+            nc1.setCapabilities(REQUIRED_CAPABILITIES, forbiddenCapabilities);
+            assertArrayEquals(REQUIRED_CAPABILITIES, nc1.getCapabilities());
+            assertArrayEquals(forbiddenCapabilities, nc1.getForbiddenCapabilities());
+
+            nc2.clearAll();
+            for (int cap : REQUIRED_CAPABILITIES) {
+                nc2.addCapability(cap);
+            }
+            for (int cap : forbiddenCapabilities) {
+                nc2.addForbiddenCapability(cap);
+            }
+            assertEquals(nc1, nc2);
+        }
+    }
+
+    @Test
+    public void testSetNetworkSpecifierOnMultiTransportNc() {
+        // Sequence 1: Transport + Transport + NetworkSpecifier
+        NetworkCapabilities nc1 = new NetworkCapabilities();
+        nc1.addTransportType(TRANSPORT_CELLULAR).addTransportType(TRANSPORT_WIFI);
+        try {
+            nc1.setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier("eth0"));
+            fail("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!");
+        } catch (IllegalStateException expected) {
+            // empty
+        }
+
+        // Sequence 2: Transport + NetworkSpecifier + Transport
+        NetworkCapabilities nc2 = new NetworkCapabilities();
+        nc2.addTransportType(TRANSPORT_CELLULAR).setNetworkSpecifier(
+                CompatUtil.makeEthernetNetworkSpecifier("testtap3"));
+        try {
+            nc2.addTransportType(TRANSPORT_WIFI);
+            fail("Cannot set a second TransportType of a network which has a NetworkSpecifier!");
+        } catch (IllegalStateException expected) {
+            // empty
+        }
+    }
+
+    @Test
+    public void testSetTransportInfoOnMultiTransportNc() {
+        // Sequence 1: Transport + Transport + TransportInfo
+        NetworkCapabilities nc1 = new NetworkCapabilities();
+        nc1.addTransportType(TRANSPORT_CELLULAR).addTransportType(TRANSPORT_WIFI)
+                .setTransportInfo(new TestTransportInfo());
+
+        // Sequence 2: Transport + NetworkSpecifier + Transport
+        NetworkCapabilities nc2 = new NetworkCapabilities();
+        nc2.addTransportType(TRANSPORT_CELLULAR).setTransportInfo(new TestTransportInfo())
+                .addTransportType(TRANSPORT_WIFI);
+    }
+
+    @Test
+    public void testCombineTransportInfo() {
+        NetworkCapabilities nc1 = new NetworkCapabilities();
+        nc1.setTransportInfo(new TestTransportInfo());
+
+        NetworkCapabilities nc2 = new NetworkCapabilities();
+        // new TransportInfo so that object is not #equals to nc1's TransportInfo (that's where
+        // combine fails)
+        nc2.setTransportInfo(new TestTransportInfo());
+
+        try {
+            nc1.combineCapabilities(nc2);
+            fail("Should not be able to combine NetworkCabilities which contain TransportInfos");
+        } catch (IllegalStateException expected) {
+            // empty
+        }
+
+        // verify that can combine with identical TransportInfo objects
+        NetworkCapabilities nc3 = new NetworkCapabilities();
+        nc3.setTransportInfo(nc1.getTransportInfo());
+        nc1.combineCapabilities(nc3);
+    }
+
+    @Test
+    public void testSet() {
+        NetworkCapabilities nc1 = new NetworkCapabilities();
+        NetworkCapabilities nc2 = new NetworkCapabilities();
+
+        if (isAtLeastS()) {
+            nc1.addForbiddenCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
+        }
+        nc1.addCapability(NET_CAPABILITY_NOT_ROAMING);
+        assertNotEquals(nc1, nc2);
+        nc2.set(nc1);
+        assertEquals(nc1, nc2);
+        assertTrue(nc2.hasCapability(NET_CAPABILITY_NOT_ROAMING));
+        if (isAtLeastS()) {
+            assertTrue(nc2.hasForbiddenCapability(NET_CAPABILITY_CAPTIVE_PORTAL));
+        }
+
+        if (isAtLeastS()) {
+            // This will effectively move NOT_ROAMING capability from required to forbidden for nc1.
+            nc1.addForbiddenCapability(NET_CAPABILITY_NOT_ROAMING);
+        }
+        nc1.setSSID(TEST_SSID);
+        nc2.set(nc1);
+        assertEquals(nc1, nc2);
+        if (isAtLeastS()) {
+            // Contrary to combineCapabilities, set() will have removed the NOT_ROAMING capability
+            // from nc2.
+            assertFalse(nc2.hasCapability(NET_CAPABILITY_NOT_ROAMING));
+            assertTrue(nc2.hasForbiddenCapability(NET_CAPABILITY_NOT_ROAMING));
+        }
+
+        if (isAtLeastR()) {
+            assertTrue(TEST_SSID.equals(nc2.getSsid()));
+        }
+
+        nc1.setSSID(DIFFERENT_TEST_SSID);
+        nc2.set(nc1);
+        assertEquals(nc1, nc2);
+        if (isAtLeastR()) {
+            assertTrue(DIFFERENT_TEST_SSID.equals(nc2.getSsid()));
+        }
+        if (isAtLeastS()) {
+            nc1.setUids(uidRanges(10, 13));
+        } else {
+            nc1.setUids(null);
+        }
+        nc2.set(nc1);  // Overwrites, as opposed to combineCapabilities
+        assertEquals(nc1, nc2);
+
+        if (isAtLeastS()) {
+            assertThrows(NullPointerException.class, () -> nc1.setSubscriptionIds(null));
+            nc1.setSubscriptionIds(Set.of());
+            nc2.set(nc1);
+            assertEquals(nc1, nc2);
+
+            nc1.setSubscriptionIds(Set.of(TEST_SUBID1));
+            nc2.set(nc1);
+            assertEquals(nc1, nc2);
+
+            nc2.setSubscriptionIds(Set.of(TEST_SUBID2, TEST_SUBID1));
+            nc2.set(nc1);
+            assertEquals(nc1, nc2);
+
+            nc2.setSubscriptionIds(Set.of(TEST_SUBID3, TEST_SUBID2));
+            assertNotEquals(nc1, nc2);
+        }
+    }
+
+    @Test
+    public void testGetTransportTypes() {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.addTransportType(TRANSPORT_CELLULAR);
+        nc.addTransportType(TRANSPORT_WIFI);
+        nc.addTransportType(TRANSPORT_VPN);
+        nc.addTransportType(TRANSPORT_TEST);
+
+        final int[] transportTypes = nc.getTransportTypes();
+        assertEquals(4, transportTypes.length);
+        assertEquals(TRANSPORT_CELLULAR, transportTypes[0]);
+        assertEquals(TRANSPORT_WIFI, transportTypes[1]);
+        assertEquals(TRANSPORT_VPN, transportTypes[2]);
+        assertEquals(TRANSPORT_TEST, transportTypes[3]);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testTelephonyNetworkSpecifier() {
+        final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+        final NetworkCapabilities nc1 = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .setNetworkSpecifier(specifier)
+                .build();
+        assertEquals(specifier, nc1.getNetworkSpecifier());
+        try {
+            final NetworkCapabilities nc2 = new NetworkCapabilities.Builder()
+                    .setNetworkSpecifier(specifier)
+                    .build();
+            fail("Must have a single transport type. Without transport type or multiple transport"
+                    + " types is invalid.");
+        } catch (IllegalStateException expected) { }
+    }
+
+    @Test
+    public void testWifiAwareNetworkSpecifier() {
+        final NetworkCapabilities nc = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_WIFI_AWARE);
+        // If NetworkSpecifier is not set, the default value is null.
+        assertNull(nc.getNetworkSpecifier());
+        final WifiAwareNetworkSpecifier specifier = new WifiAwareNetworkSpecifier.Builder(
+                mDiscoverySession, mPeerHandle).build();
+        nc.setNetworkSpecifier(specifier);
+        assertEquals(specifier, nc.getNetworkSpecifier());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testAdministratorUidsAndOwnerUid() {
+        // Test default owner uid.
+        // If the owner uid is not set, the default value should be Process.INVALID_UID.
+        final NetworkCapabilities nc1 = new NetworkCapabilities.Builder().build();
+        assertEquals(INVALID_UID, nc1.getOwnerUid());
+        // Test setAdministratorUids and getAdministratorUids.
+        final int[] administratorUids = {1001, 10001};
+        final NetworkCapabilities nc2 = new NetworkCapabilities.Builder()
+                .setAdministratorUids(administratorUids)
+                .build();
+        assertTrue(Arrays.equals(administratorUids, nc2.getAdministratorUids()));
+        // Test setOwnerUid and getOwnerUid.
+        // The owner UID must be included in administrator UIDs, or throw IllegalStateException.
+        try {
+            final NetworkCapabilities nc3 = new NetworkCapabilities.Builder()
+                    .setOwnerUid(1001)
+                    .build();
+            fail("The owner UID must be included in administrator UIDs.");
+        } catch (IllegalStateException expected) { }
+        final NetworkCapabilities nc4 = new NetworkCapabilities.Builder()
+                .setAdministratorUids(administratorUids)
+                .setOwnerUid(1001)
+                .build();
+        assertEquals(1001, nc4.getOwnerUid());
+        try {
+            final NetworkCapabilities nc5 = new NetworkCapabilities.Builder()
+                    .setAdministratorUids(null)
+                    .build();
+            fail("Should not set null into setAdministratorUids");
+        } catch (NullPointerException expected) { }
+    }
+
+    private static NetworkCapabilities capsWithSubIds(Integer ... subIds) {
+        // Since the NetworkRequest would put NOT_VCN_MANAGED capabilities in general, for
+        // every NetworkCapabilities that simulates networks needs to add it too in order to
+        // satisfy these requests.
+        final NetworkCapabilities nc = new NetworkCapabilities.Builder()
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .setSubscriptionIds(new ArraySet<>(subIds)).build();
+        assertEquals(new ArraySet<>(subIds), nc.getSubscriptionIds());
+        return nc;
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testSubIds() throws Exception {
+        final NetworkCapabilities ncWithoutId = capsWithSubIds();
+        final NetworkCapabilities ncWithId = capsWithSubIds(TEST_SUBID1);
+        final NetworkCapabilities ncWithOtherIds = capsWithSubIds(TEST_SUBID1, TEST_SUBID3);
+        final NetworkCapabilities ncWithoutRequestedIds = capsWithSubIds(TEST_SUBID3);
+
+        final NetworkRequest requestWithoutId = new NetworkRequest.Builder().build();
+        assertEmpty(requestWithoutId.networkCapabilities.getSubscriptionIds());
+        final NetworkRequest requestWithIds = new NetworkRequest.Builder()
+                .setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2)).build();
+        assertEquals(Set.of(TEST_SUBID1, TEST_SUBID2),
+                requestWithIds.networkCapabilities.getSubscriptionIds());
+
+        assertFalse(requestWithIds.canBeSatisfiedBy(ncWithoutId));
+        assertTrue(requestWithIds.canBeSatisfiedBy(ncWithOtherIds));
+        assertFalse(requestWithIds.canBeSatisfiedBy(ncWithoutRequestedIds));
+        assertTrue(requestWithIds.canBeSatisfiedBy(ncWithId));
+        assertTrue(requestWithoutId.canBeSatisfiedBy(ncWithoutId));
+        assertTrue(requestWithoutId.canBeSatisfiedBy(ncWithId));
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testEqualsSubIds() throws Exception {
+        assertEquals(capsWithSubIds(), capsWithSubIds());
+        assertNotEquals(capsWithSubIds(), capsWithSubIds(TEST_SUBID1));
+        assertEquals(capsWithSubIds(TEST_SUBID1), capsWithSubIds(TEST_SUBID1));
+        assertNotEquals(capsWithSubIds(TEST_SUBID1), capsWithSubIds(TEST_SUBID2));
+        assertNotEquals(capsWithSubIds(TEST_SUBID1), capsWithSubIds(TEST_SUBID2, TEST_SUBID1));
+        assertEquals(capsWithSubIds(TEST_SUBID1, TEST_SUBID2),
+                capsWithSubIds(TEST_SUBID2, TEST_SUBID1));
+    }
+
+    @Test
+    public void testLinkBandwidthKbps() {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        // The default value of LinkDown/UpstreamBandwidthKbps should be LINK_BANDWIDTH_UNSPECIFIED.
+        assertEquals(LINK_BANDWIDTH_UNSPECIFIED, nc.getLinkDownstreamBandwidthKbps());
+        assertEquals(LINK_BANDWIDTH_UNSPECIFIED, nc.getLinkUpstreamBandwidthKbps());
+        nc.setLinkDownstreamBandwidthKbps(512);
+        nc.setLinkUpstreamBandwidthKbps(128);
+        assertEquals(512, nc.getLinkDownstreamBandwidthKbps());
+        assertNotEquals(128, nc.getLinkDownstreamBandwidthKbps());
+        assertEquals(128, nc.getLinkUpstreamBandwidthKbps());
+        assertNotEquals(512, nc.getLinkUpstreamBandwidthKbps());
+    }
+
+    @Test
+    public void testSignalStrength() {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        // The default value of signal strength should be SIGNAL_STRENGTH_UNSPECIFIED.
+        assertEquals(SIGNAL_STRENGTH_UNSPECIFIED, nc.getSignalStrength());
+        nc.setSignalStrength(-80);
+        assertEquals(-80, nc.getSignalStrength());
+        assertNotEquals(-50, nc.getSignalStrength());
+    }
+
+    private void assertNoTransport(NetworkCapabilities nc) {
+        for (int i = MIN_TRANSPORT; i <= MAX_TRANSPORT; i++) {
+            assertFalse(nc.hasTransport(i));
+        }
+    }
+
+    // Checks that all transport types from MIN_TRANSPORT to maxTransportType are set and all
+    // transport types from maxTransportType + 1 to MAX_TRANSPORT are not set when positiveSequence
+    // is true. If positiveSequence is false, then the check sequence is opposite.
+    private void checkCurrentTransportTypes(NetworkCapabilities nc, int maxTransportType,
+            boolean positiveSequence) {
+        for (int i = MIN_TRANSPORT; i <= maxTransportType; i++) {
+            if (positiveSequence) {
+                assertTrue(nc.hasTransport(i));
+            } else {
+                assertFalse(nc.hasTransport(i));
+            }
+        }
+        for (int i = MAX_TRANSPORT; i > maxTransportType; i--) {
+            if (positiveSequence) {
+                assertFalse(nc.hasTransport(i));
+            } else {
+                assertTrue(nc.hasTransport(i));
+            }
+        }
+    }
+
+    @Test
+    public void testMultipleTransportTypes() {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        assertNoTransport(nc);
+        // Test adding multiple transport types.
+        for (int i = MIN_TRANSPORT; i <= MAX_TRANSPORT; i++) {
+            nc.addTransportType(i);
+            checkCurrentTransportTypes(nc, i, true /* positiveSequence */);
+        }
+        // Test removing multiple transport types.
+        for (int i = MIN_TRANSPORT; i <= MAX_TRANSPORT; i++) {
+            nc.removeTransportType(i);
+            checkCurrentTransportTypes(nc, i, false /* positiveSequence */);
+        }
+        assertNoTransport(nc);
+        nc.addTransportType(TRANSPORT_WIFI);
+        assertTrue(nc.hasTransport(TRANSPORT_WIFI));
+        assertFalse(nc.hasTransport(TRANSPORT_VPN));
+        nc.addTransportType(TRANSPORT_VPN);
+        assertTrue(nc.hasTransport(TRANSPORT_WIFI));
+        assertTrue(nc.hasTransport(TRANSPORT_VPN));
+        nc.removeTransportType(TRANSPORT_WIFI);
+        assertFalse(nc.hasTransport(TRANSPORT_WIFI));
+        assertTrue(nc.hasTransport(TRANSPORT_VPN));
+        nc.removeTransportType(TRANSPORT_VPN);
+        assertFalse(nc.hasTransport(TRANSPORT_WIFI));
+        assertFalse(nc.hasTransport(TRANSPORT_VPN));
+        assertNoTransport(nc);
+    }
+
+    @Test
+    public void testAddAndRemoveTransportType() {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        try {
+            nc.addTransportType(-1);
+            fail("Should not set invalid transport type into addTransportType");
+        } catch (IllegalArgumentException expected) { }
+        try {
+            nc.removeTransportType(-1);
+            fail("Should not set invalid transport type into removeTransportType");
+        } catch (IllegalArgumentException e) { }
+    }
+
+    /**
+     * Test TransportInfo to verify redaction mechanism.
+     */
+    private static class TestTransportInfo implements TransportInfo {
+        public final boolean locationRedacted;
+        public final boolean localMacAddressRedacted;
+        public final boolean settingsRedacted;
+
+        TestTransportInfo() {
+            locationRedacted = false;
+            localMacAddressRedacted = false;
+            settingsRedacted = false;
+        }
+
+        TestTransportInfo(boolean locationRedacted,
+                boolean localMacAddressRedacted,
+                boolean settingsRedacted) {
+            this.locationRedacted = locationRedacted;
+            this.localMacAddressRedacted =
+                    localMacAddressRedacted;
+            this.settingsRedacted = settingsRedacted;
+        }
+
+        @Override
+        public TransportInfo makeCopy(@NetworkCapabilities.RedactionType long redactions) {
+            return new TestTransportInfo(
+                    (redactions & NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION) != 0,
+                    (redactions & REDACT_FOR_LOCAL_MAC_ADDRESS) != 0,
+                    (redactions & REDACT_FOR_NETWORK_SETTINGS) != 0
+            );
+        }
+
+        @Override
+        public @NetworkCapabilities.RedactionType long getApplicableRedactions() {
+            return REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS
+                    | REDACT_FOR_NETWORK_SETTINGS;
+        }
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testBuilder() {
+        final int ownerUid = 1001;
+        final int signalStrength = -80;
+        final int requestUid = 10100;
+        final int[] administratorUids = {ownerUid, 10001};
+        final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
+        final TransportInfo transportInfo = new TransportInfo() {};
+        final String ssid = "TEST_SSID";
+        final String packageName = "com.google.test.networkcapabilities";
+        final NetworkCapabilities nc = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .addTransportType(TRANSPORT_CELLULAR)
+                .removeTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_EIMS)
+                .addCapability(NET_CAPABILITY_CBS)
+                .removeCapability(NET_CAPABILITY_CBS)
+                .setAdministratorUids(administratorUids)
+                .setOwnerUid(ownerUid)
+                .setLinkDownstreamBandwidthKbps(512)
+                .setLinkUpstreamBandwidthKbps(128)
+                .setNetworkSpecifier(specifier)
+                .setTransportInfo(transportInfo)
+                .setSignalStrength(signalStrength)
+                .setSsid(ssid)
+                .setRequestorUid(requestUid)
+                .setRequestorPackageName(packageName)
+                .build();
+        assertEquals(1, nc.getTransportTypes().length);
+        assertEquals(TRANSPORT_WIFI, nc.getTransportTypes()[0]);
+        assertTrue(nc.hasCapability(NET_CAPABILITY_EIMS));
+        assertFalse(nc.hasCapability(NET_CAPABILITY_CBS));
+        assertTrue(Arrays.equals(administratorUids, nc.getAdministratorUids()));
+        assertEquals(ownerUid, nc.getOwnerUid());
+        assertEquals(512, nc.getLinkDownstreamBandwidthKbps());
+        assertNotEquals(128, nc.getLinkDownstreamBandwidthKbps());
+        assertEquals(128, nc.getLinkUpstreamBandwidthKbps());
+        assertNotEquals(512, nc.getLinkUpstreamBandwidthKbps());
+        assertEquals(specifier, nc.getNetworkSpecifier());
+        assertEquals(transportInfo, nc.getTransportInfo());
+        assertEquals(signalStrength, nc.getSignalStrength());
+        assertNotEquals(-50, nc.getSignalStrength());
+        assertEquals(ssid, nc.getSsid());
+        assertEquals(requestUid, nc.getRequestorUid());
+        assertEquals(packageName, nc.getRequestorPackageName());
+        // Cannot assign null into NetworkCapabilities.Builder
+        try {
+            final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(null);
+            fail("Should not set null into NetworkCapabilities.Builder");
+        } catch (NullPointerException expected) { }
+        assertEquals(nc, new NetworkCapabilities.Builder(nc).build());
+
+        if (isAtLeastS()) {
+            final NetworkCapabilities nc2 = new NetworkCapabilities.Builder()
+                    .setSubscriptionIds(Set.of(TEST_SUBID1)).build();
+            assertEquals(Set.of(TEST_SUBID1), nc2.getSubscriptionIds());
+        }
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testBuilderWithoutDefaultCap() {
+        final NetworkCapabilities nc =
+                NetworkCapabilities.Builder.withoutDefaultCapabilities().build();
+        assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        assertFalse(nc.hasCapability(NET_CAPABILITY_TRUSTED));
+        assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_VPN));
+        // Ensure test case fails if new net cap is added into default cap but no update here.
+        assertEquals(0, nc.getCapabilities().length);
+    }
+}
diff --git a/tests/common/java/android/net/NetworkProviderTest.kt b/tests/common/java/android/net/NetworkProviderTest.kt
new file mode 100644
index 0000000..8cea12e
--- /dev/null
+++ b/tests/common/java/android/net/NetworkProviderTest.kt
@@ -0,0 +1,398 @@
+/*
+ * 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
+
+import android.app.Instrumentation
+import android.content.Context
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkProviderTest.TestNetworkCallback.CallbackEntry.OnUnavailable
+import android.net.NetworkProviderTest.TestNetworkProvider.CallbackEntry.OnNetworkRequestWithdrawn
+import android.net.NetworkProviderTest.TestNetworkProvider.CallbackEntry.OnNetworkRequested
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.util.Log
+import androidx.test.InstrumentationRegistry
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.testutils.CompatUtil
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkOfferCallback
+import com.android.testutils.isDevSdkInRange
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verifyNoMoreInteractions
+import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.concurrent.RejectedExecutionException
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.fail
+
+private const val DEFAULT_TIMEOUT_MS = 5000L
+private const val DEFAULT_NO_CALLBACK_TIMEOUT_MS = 200L
+private val instrumentation: Instrumentation
+    get() = InstrumentationRegistry.getInstrumentation()
+private val context: Context get() = InstrumentationRegistry.getContext()
+private val PROVIDER_NAME = "NetworkProviderTest"
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.Q)
+class NetworkProviderTest {
+    @Rule @JvmField
+    val mIgnoreRule = DevSdkIgnoreRule()
+    private val mCm = context.getSystemService(ConnectivityManager::class.java)
+    private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread")
+
+    @Before
+    fun setUp() {
+        instrumentation.getUiAutomation().adoptShellPermissionIdentity()
+        mHandlerThread.start()
+    }
+
+    @After
+    fun tearDown() {
+        mHandlerThread.quitSafely()
+        instrumentation.getUiAutomation().dropShellPermissionIdentity()
+    }
+
+    private class TestNetworkProvider(context: Context, looper: Looper) :
+            NetworkProvider(context, looper, PROVIDER_NAME) {
+        private val TAG = this::class.simpleName
+        private val seenEvents = ArrayTrackRecord<CallbackEntry>().newReadHead()
+
+        sealed class CallbackEntry {
+            data class OnNetworkRequested(
+                val request: NetworkRequest,
+                val score: Int,
+                val id: Int
+            ) : CallbackEntry()
+            data class OnNetworkRequestWithdrawn(val request: NetworkRequest) : CallbackEntry()
+        }
+
+        override fun onNetworkRequested(request: NetworkRequest, score: Int, id: Int) {
+            Log.d(TAG, "onNetworkRequested $request, $score, $id")
+            seenEvents.add(OnNetworkRequested(request, score, id))
+        }
+
+        override fun onNetworkRequestWithdrawn(request: NetworkRequest) {
+            Log.d(TAG, "onNetworkRequestWithdrawn $request")
+            seenEvents.add(OnNetworkRequestWithdrawn(request))
+        }
+
+        inline fun <reified T : CallbackEntry> eventuallyExpectCallbackThat(
+            crossinline predicate: (T) -> Boolean
+        ) = seenEvents.poll(DEFAULT_TIMEOUT_MS) { it is T && predicate(it) }
+                ?: fail("Did not receive callback after ${DEFAULT_TIMEOUT_MS}ms")
+
+        fun assertNoCallback() {
+            val cb = seenEvents.poll(DEFAULT_NO_CALLBACK_TIMEOUT_MS)
+            if (null != cb) fail("Expected no callback but got $cb")
+        }
+    }
+
+    private fun createNetworkProvider(ctx: Context = context): TestNetworkProvider {
+        return TestNetworkProvider(ctx, mHandlerThread.looper)
+    }
+
+    private fun createAndRegisterNetworkProvider(ctx: Context = context) =
+        createNetworkProvider(ctx).also {
+            assertEquals(it.getProviderId(), NetworkProvider.ID_NONE)
+            mCm.registerNetworkProvider(it)
+            assertNotEquals(it.getProviderId(), NetworkProvider.ID_NONE)
+        }
+
+    // In S+ framework, do not run this test, since the provider will no longer receive
+    // onNetworkRequested for every request. Instead, provider needs to
+    // call {@code registerNetworkOffer} with the description of networks they
+    // might have ability to setup, and expects {@link NetworkOfferCallback#onNetworkNeeded}.
+    @IgnoreAfter(Build.VERSION_CODES.R)
+    @Test
+    fun testOnNetworkRequested() {
+        val provider = createAndRegisterNetworkProvider()
+
+        val specifier = CompatUtil.makeTestNetworkSpecifier(
+                UUID.randomUUID().toString())
+        // Test network is not allowed to be trusted.
+        val nr: NetworkRequest = NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_TEST)
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .setNetworkSpecifier(specifier)
+                .build()
+        val cb = ConnectivityManager.NetworkCallback()
+        mCm.requestNetwork(nr, cb)
+        provider.eventuallyExpectCallbackThat<OnNetworkRequested>() { callback ->
+            callback.request.getNetworkSpecifier() == specifier &&
+            callback.request.hasTransport(TRANSPORT_TEST)
+        }
+
+        val initialScore = 40
+        val updatedScore = 60
+        val nc = NetworkCapabilities().apply {
+                addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+                addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+                addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                setNetworkSpecifier(specifier)
+        }
+        val lp = LinkProperties()
+        val config = NetworkAgentConfig.Builder().build()
+        val agent = object : NetworkAgent(context, mHandlerThread.looper, "TestAgent", nc, lp,
+                initialScore, config, provider) {}
+        agent.register()
+        agent.markConnected()
+
+        provider.eventuallyExpectCallbackThat<OnNetworkRequested>() { callback ->
+            callback.request.getNetworkSpecifier() == specifier &&
+            callback.score == initialScore &&
+            callback.id == agent.providerId
+        }
+
+        agent.sendNetworkScore(updatedScore)
+        provider.eventuallyExpectCallbackThat<OnNetworkRequested>() { callback ->
+            callback.request.getNetworkSpecifier() == specifier &&
+            callback.score == updatedScore &&
+            callback.id == agent.providerId
+        }
+
+        mCm.unregisterNetworkCallback(cb)
+        provider.eventuallyExpectCallbackThat<OnNetworkRequestWithdrawn>() { callback ->
+            callback.request.getNetworkSpecifier() == specifier &&
+            callback.request.hasTransport(TRANSPORT_TEST)
+        }
+        mCm.unregisterNetworkProvider(provider)
+        // Provider id should be ID_NONE after unregister network provider
+        assertEquals(provider.getProviderId(), NetworkProvider.ID_NONE)
+        // unregisterNetworkProvider should not crash even if it's called on an
+        // already unregistered provider.
+        mCm.unregisterNetworkProvider(provider)
+    }
+
+    // Mainline module can't use internal HandlerExecutor, so add an identical executor here.
+    // TODO: Refactor with the one in MultiNetworkPolicyTracker.
+    private class HandlerExecutor(private val handler: Handler) : Executor {
+        public override fun execute(command: Runnable) {
+            if (!handler.post(command)) {
+                throw RejectedExecutionException(handler.toString() + " is shutting down")
+            }
+        }
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    fun testRegisterNetworkOffer() {
+        val provider = createAndRegisterNetworkProvider()
+        val provider2 = createAndRegisterNetworkProvider()
+
+        // Prepare the materials which will be used to create different offers.
+        val specifier1 = CompatUtil.makeTestNetworkSpecifier("TEST-SPECIFIER-1")
+        val specifier2 = CompatUtil.makeTestNetworkSpecifier("TEST-SPECIFIER-2")
+        val scoreWeaker = NetworkScore.Builder().build()
+        val scoreStronger = NetworkScore.Builder().setTransportPrimary(true).build()
+        val ncFilter1 = NetworkCapabilities.Builder().addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(specifier1).build()
+        val ncFilter2 = NetworkCapabilities.Builder().addTransportType(TRANSPORT_TEST)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .setNetworkSpecifier(specifier1).build()
+        val ncFilter3 = NetworkCapabilities.Builder().addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(specifier2).build()
+        val ncFilter4 = NetworkCapabilities.Builder().addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(specifier2).build()
+
+        // Make 4 offers, where 1 doesn't have NOT_VCN, 2 has NOT_VCN, 3 is similar to 1 but with
+        // different specifier, and 4 is also similar to 1 but with different provider.
+        val offerCallback1 = TestableNetworkOfferCallback(
+                DEFAULT_TIMEOUT_MS, DEFAULT_NO_CALLBACK_TIMEOUT_MS)
+        val offerCallback2 = TestableNetworkOfferCallback(
+                DEFAULT_TIMEOUT_MS, DEFAULT_NO_CALLBACK_TIMEOUT_MS)
+        val offerCallback3 = TestableNetworkOfferCallback(
+                DEFAULT_TIMEOUT_MS, DEFAULT_NO_CALLBACK_TIMEOUT_MS)
+        val offerCallback4 = TestableNetworkOfferCallback(
+                DEFAULT_TIMEOUT_MS, DEFAULT_NO_CALLBACK_TIMEOUT_MS)
+        provider.registerNetworkOffer(scoreWeaker, ncFilter1,
+                HandlerExecutor(mHandlerThread.threadHandler), offerCallback1)
+        provider.registerNetworkOffer(scoreStronger, ncFilter2,
+                HandlerExecutor(mHandlerThread.threadHandler), offerCallback2)
+        provider.registerNetworkOffer(scoreWeaker, ncFilter3,
+                HandlerExecutor(mHandlerThread.threadHandler), offerCallback3)
+        provider2.registerNetworkOffer(scoreWeaker, ncFilter4,
+                HandlerExecutor(mHandlerThread.threadHandler), offerCallback4)
+        // Unlike Android R, Android S+ provider will only receive interested requests via offer
+        // callback. Verify that the callbacks do not see any existing request such as default
+        // requests.
+        offerCallback1.assertNoCallback()
+        offerCallback2.assertNoCallback()
+        offerCallback3.assertNoCallback()
+        offerCallback4.assertNoCallback()
+
+        // File a request with specifier but without NOT_VCN, verify network is needed for callback
+        // with the same specifier.
+        val nrNoNotVcn: NetworkRequest = NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_TEST)
+                // Test network is not allowed to be trusted.
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .setNetworkSpecifier(specifier1)
+                .build()
+        val cb1 = ConnectivityManager.NetworkCallback()
+        mCm.requestNetwork(nrNoNotVcn, cb1)
+        offerCallback1.expectOnNetworkNeeded(ncFilter1)
+        offerCallback2.expectOnNetworkNeeded(ncFilter2)
+        offerCallback3.assertNoCallback()
+        offerCallback4.assertNoCallback()
+
+        mCm.unregisterNetworkCallback(cb1)
+        offerCallback1.expectOnNetworkUnneeded(ncFilter1)
+        offerCallback2.expectOnNetworkUnneeded(ncFilter2)
+        offerCallback3.assertNoCallback()
+        offerCallback4.assertNoCallback()
+
+        // File a request without specifier but with NOT_VCN, verify network is needed for offer
+        // with NOT_VCN.
+        val nrNotVcn: NetworkRequest = NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_TEST)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                // Test network is not allowed to be trusted.
+                .removeCapability(NET_CAPABILITY_TRUSTED)
+                .build()
+        val cb2 = ConnectivityManager.NetworkCallback()
+        mCm.requestNetwork(nrNotVcn, cb2)
+        offerCallback1.assertNoCallback()
+        offerCallback2.expectOnNetworkNeeded(ncFilter2)
+        offerCallback3.assertNoCallback()
+        offerCallback4.assertNoCallback()
+
+        // Upgrade offer 3 & 4 to satisfy previous request and then verify they are also needed.
+        ncFilter3.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        provider.registerNetworkOffer(scoreWeaker, ncFilter3,
+                HandlerExecutor(mHandlerThread.threadHandler), offerCallback3)
+        ncFilter4.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        provider2.registerNetworkOffer(scoreWeaker, ncFilter4,
+                HandlerExecutor(mHandlerThread.threadHandler), offerCallback4)
+        offerCallback1.assertNoCallback()
+        offerCallback2.assertNoCallback()
+        offerCallback3.expectOnNetworkNeeded(ncFilter3)
+        offerCallback4.expectOnNetworkNeeded(ncFilter4)
+
+        // Connect an agent to fulfill the request, verify offer 4 is not needed since it is not
+        // from currently serving provider nor can beat the current satisfier.
+        val nc = NetworkCapabilities().apply {
+            addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+            removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+            addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+            addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+            addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+            addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+            setNetworkSpecifier(specifier1)
+        }
+        val config = NetworkAgentConfig.Builder().build()
+        val agent = object : NetworkAgent(context, mHandlerThread.looper, "TestAgent", nc,
+                LinkProperties(), scoreWeaker, config, provider) {}
+        agent.register()
+        agent.markConnected()
+        // TODO: The request is satisying by offer 2 instead of offer 1, thus it should not be
+        //  considered as needed.
+        offerCallback1.expectOnNetworkNeeded(ncFilter2)
+        offerCallback2.assertNoCallback()  // Still needed.
+        offerCallback3.assertNoCallback()  // Still needed.
+        offerCallback4.expectOnNetworkUnneeded(ncFilter4)
+
+        // Upgrade the agent, verify no change since the framework will treat the offer as needed
+        // if a request is currently satisfied by the network provided by the same provider.
+        // TODO: Consider offers with weaker score are unneeded.
+        agent.sendNetworkScore(scoreStronger)
+        offerCallback1.assertNoCallback()
+        offerCallback2.assertNoCallback()  // Still needed.
+        offerCallback3.assertNoCallback()  // Still needed.
+        offerCallback4.assertNoCallback()  // Still unneeded.
+
+        // Verify that offer callbacks cannot receive any event if offer is unregistered.
+        provider2.unregisterNetworkOffer(offerCallback4)
+        agent.unregister()
+        offerCallback1.assertNoCallback()  // Still needed.
+        offerCallback2.assertNoCallback()  // Still needed.
+        offerCallback3.assertNoCallback()  // Still needed.
+        // Since the agent is unregistered, and the offer has chance to satisfy the request,
+        // this callback should receive needed if it is not unregistered.
+        offerCallback4.assertNoCallback()
+
+        // Verify that offer callbacks cannot receive any event if provider is unregistered.
+        mCm.unregisterNetworkProvider(provider)
+        mCm.unregisterNetworkCallback(cb2)
+        offerCallback1.assertNoCallback()  // Should be unneeded if not unregistered.
+        offerCallback2.assertNoCallback()  // Should be unneeded if not unregistered.
+        offerCallback3.assertNoCallback()  // Should be unneeded if not unregistered.
+        offerCallback4.assertNoCallback()  // Already unregistered.
+
+        // Clean up and Verify providers did not receive any callback during the entire test.
+        mCm.unregisterNetworkProvider(provider2)
+        provider.assertNoCallback()
+        provider2.assertNoCallback()
+    }
+
+    private class TestNetworkCallback : ConnectivityManager.NetworkCallback() {
+        private val seenEvents = ArrayTrackRecord<CallbackEntry>().newReadHead()
+        sealed class CallbackEntry {
+            object OnUnavailable : CallbackEntry()
+        }
+
+        override fun onUnavailable() {
+            seenEvents.add(OnUnavailable)
+        }
+
+        inline fun <reified T : CallbackEntry> expectCallback(
+            crossinline predicate: (T) -> Boolean
+        ) = seenEvents.poll(DEFAULT_TIMEOUT_MS) { it is T && predicate(it) }
+    }
+
+    @Test
+    fun testDeclareNetworkRequestUnfulfillable() {
+        val mockContext = mock(Context::class.java)
+        doReturn(mCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE)
+        val provider = createNetworkProvider(mockContext)
+        // ConnectivityManager not required at creation time after R
+        if (!isDevSdkInRange(0, Build.VERSION_CODES.R)) {
+            verifyNoMoreInteractions(mockContext)
+        }
+
+        mCm.registerNetworkProvider(provider)
+
+        val specifier = CompatUtil.makeTestNetworkSpecifier(
+                UUID.randomUUID().toString())
+        val nr: NetworkRequest = NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(specifier)
+                .build()
+
+        val cb = TestNetworkCallback()
+        mCm.requestNetwork(nr, cb)
+        provider.declareNetworkRequestUnfulfillable(nr)
+        cb.expectCallback<OnUnavailable>() { nr.getNetworkSpecifier() == specifier }
+        mCm.unregisterNetworkProvider(provider)
+    }
+}
diff --git a/tests/common/java/android/net/NetworkSpecifierTest.kt b/tests/common/java/android/net/NetworkSpecifierTest.kt
new file mode 100644
index 0000000..f3409f5
--- /dev/null
+++ b/tests/common/java/android/net/NetworkSpecifierTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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
+
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertTrue
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.Q)
+class NetworkSpecifierTest {
+    private class TestNetworkSpecifier(
+        val intData: Int = 123,
+        val stringData: String = "init"
+    ) : NetworkSpecifier() {
+        override fun canBeSatisfiedBy(other: NetworkSpecifier?): Boolean =
+                other != null &&
+                other is TestNetworkSpecifier &&
+                other.intData >= intData &&
+                stringData.equals(other.stringData)
+
+        override fun redact(): NetworkSpecifier = TestNetworkSpecifier(intData, "redact")
+    }
+
+    @Test
+    fun testRedact() {
+        val ns: TestNetworkSpecifier = TestNetworkSpecifier()
+        val redactNs = ns.redact()
+        assertTrue(redactNs is TestNetworkSpecifier)
+        assertEquals(ns.intData, redactNs.intData)
+        assertNotEquals(ns.stringData, redactNs.stringData)
+        assertTrue("redact".equals(redactNs.stringData))
+    }
+
+    @Test
+    fun testcanBeSatisfiedBy() {
+        val target: TestNetworkSpecifier = TestNetworkSpecifier()
+        assertFalse(target.canBeSatisfiedBy(null))
+        assertTrue(target.canBeSatisfiedBy(TestNetworkSpecifier()))
+        val otherNs = TelephonyNetworkSpecifier.Builder().setSubscriptionId(123).build()
+        assertFalse(target.canBeSatisfiedBy(otherNs))
+        assertTrue(target.canBeSatisfiedBy(TestNetworkSpecifier(intData = 999)))
+        assertFalse(target.canBeSatisfiedBy(TestNetworkSpecifier(intData = 1)))
+        assertFalse(target.canBeSatisfiedBy(TestNetworkSpecifier(stringData = "diff")))
+    }
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/NetworkStackTest.java b/tests/common/java/android/net/NetworkStackTest.java
new file mode 100644
index 0000000..f8f9c72
--- /dev/null
+++ b/tests/common/java/android/net/NetworkStackTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Build;
+import android.os.IBinder;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkStackTest {
+    @Rule
+    public DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule();
+
+    @Mock private IBinder mConnectorBinder;
+
+    @Before public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testGetService() {
+        NetworkStack.setServiceForTest(mConnectorBinder);
+        assertEquals(NetworkStack.getService(), mConnectorBinder);
+    }
+}
diff --git a/tests/common/java/android/net/NetworkStateSnapshotTest.kt b/tests/common/java/android/net/NetworkStateSnapshotTest.kt
new file mode 100644
index 0000000..0ca4d95
--- /dev/null
+++ b/tests/common/java/android/net/NetworkStateSnapshotTest.kt
@@ -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.net.ConnectivityManager.TYPE_NONE
+import android.net.ConnectivityManager.TYPE_WIFI
+import android.net.InetAddresses.parseNumericAddress
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelSane
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.Inet4Address
+import java.net.Inet6Address
+
+private const val TEST_IMSI = "imsi1"
+private const val TEST_SSID = "SSID1"
+private const val TEST_NETID = 123
+
+private val TEST_IPV4_GATEWAY = parseNumericAddress("192.168.222.3") as Inet4Address
+private val TEST_IPV6_GATEWAY = parseNumericAddress("2001:db8::1") as Inet6Address
+private val TEST_IPV4_LINKADDR = LinkAddress("192.168.222.123/24")
+private val TEST_IPV6_LINKADDR = LinkAddress("2001:db8::123/64")
+private val TEST_IFACE = "fake0"
+private val TEST_LINK_PROPERTIES = LinkProperties().apply {
+    interfaceName = TEST_IFACE
+    addLinkAddress(TEST_IPV4_LINKADDR)
+    addLinkAddress(TEST_IPV6_LINKADDR)
+
+    // Add default routes
+    addRoute(RouteInfo(IpPrefix(parseNumericAddress("0.0.0.0"), 0), TEST_IPV4_GATEWAY))
+    addRoute(RouteInfo(IpPrefix(parseNumericAddress("::"), 0), TEST_IPV6_GATEWAY))
+}
+
+private val TEST_CAPABILITIES = NetworkCapabilities().apply {
+    addTransportType(TRANSPORT_WIFI)
+    setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false)
+    setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true)
+    setSSID(TEST_SSID)
+}
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class NetworkStateSnapshotTest {
+
+    @Test
+    fun testParcelUnparcel() {
+        val emptySnapshot = NetworkStateSnapshot(Network(TEST_NETID), NetworkCapabilities(),
+                LinkProperties(), null, TYPE_NONE)
+        val snapshot = NetworkStateSnapshot(
+                Network(TEST_NETID), TEST_CAPABILITIES, TEST_LINK_PROPERTIES, TEST_IMSI, TYPE_WIFI)
+        assertParcelSane(emptySnapshot, 5)
+        assertParcelSane(snapshot, 5)
+    }
+}
diff --git a/tests/common/java/android/net/NetworkTest.java b/tests/common/java/android/net/NetworkTest.java
new file mode 100644
index 0000000..11d44b8
--- /dev/null
+++ b/tests/common/java/android/net/NetworkTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.net.DatagramSocket;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketException;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkTest {
+    final Network mNetwork = new Network(99);
+
+    @Test
+    public void testBindSocketOfInvalidFdThrows() throws Exception {
+
+        final FileDescriptor fd = new FileDescriptor();
+        assertFalse(fd.valid());
+
+        try {
+            mNetwork.bindSocket(fd);
+            fail("SocketException not thrown");
+        } catch (SocketException expected) {}
+    }
+
+    @Test
+    public void testBindSocketOfNonSocketFdThrows() throws Exception {
+        final File devNull = new File("/dev/null");
+        assertTrue(devNull.canRead());
+
+        final FileInputStream fis = new FileInputStream(devNull);
+        assertTrue(null != fis.getFD());
+        assertTrue(fis.getFD().valid());
+
+        try {
+            mNetwork.bindSocket(fis.getFD());
+            fail("SocketException not thrown");
+        } catch (SocketException expected) {}
+    }
+
+    @Test
+    @AppModeFull(reason = "Socket cannot bind in instant app mode")
+    public void testBindSocketOfConnectedDatagramSocketThrows() throws Exception {
+        final DatagramSocket mDgramSocket = new DatagramSocket(0, (InetAddress) Inet6Address.ANY);
+        mDgramSocket.connect((InetAddress) Inet6Address.LOOPBACK, 53);
+        assertTrue(mDgramSocket.isConnected());
+
+        try {
+            mNetwork.bindSocket(mDgramSocket);
+            fail("SocketException not thrown");
+        } catch (SocketException expected) {}
+    }
+
+    @Test
+    public void testBindSocketOfLocalSocketThrows() throws Exception {
+        final LocalSocket mLocalClient = new LocalSocket();
+        mLocalClient.bind(new LocalSocketAddress("testClient"));
+        assertTrue(mLocalClient.getFileDescriptor().valid());
+
+        try {
+            mNetwork.bindSocket(mLocalClient.getFileDescriptor());
+            fail("SocketException not thrown");
+        } catch (SocketException expected) {}
+
+        final LocalServerSocket mLocalServer = new LocalServerSocket("testServer");
+        mLocalClient.connect(mLocalServer.getLocalSocketAddress());
+        assertTrue(mLocalClient.isConnected());
+
+        try {
+            mNetwork.bindSocket(mLocalClient.getFileDescriptor());
+            fail("SocketException not thrown");
+        } catch (SocketException expected) {}
+    }
+
+    @Test
+    public void testZeroIsObviousForDebugging() {
+        Network zero = new Network(0);
+        assertEquals(0, zero.hashCode());
+        assertEquals(0, zero.getNetworkHandle());
+        assertEquals("0", zero.toString());
+    }
+
+    @Test
+    public void testGetNetworkHandle() {
+        Network one = new Network(1);
+        Network two = new Network(2);
+        Network three = new Network(3);
+
+        // None of the hashcodes are zero.
+        assertNotEquals(0, one.hashCode());
+        assertNotEquals(0, two.hashCode());
+        assertNotEquals(0, three.hashCode());
+
+        // All the hashcodes are distinct.
+        assertNotEquals(one.hashCode(), two.hashCode());
+        assertNotEquals(one.hashCode(), three.hashCode());
+        assertNotEquals(two.hashCode(), three.hashCode());
+
+        // None of the handles are zero.
+        assertNotEquals(0, one.getNetworkHandle());
+        assertNotEquals(0, two.getNetworkHandle());
+        assertNotEquals(0, three.getNetworkHandle());
+
+        // All the handles are distinct.
+        assertNotEquals(one.getNetworkHandle(), two.getNetworkHandle());
+        assertNotEquals(one.getNetworkHandle(), three.getNetworkHandle());
+        assertNotEquals(two.getNetworkHandle(), three.getNetworkHandle());
+
+        // The handles are not equal to the hashcodes.
+        assertNotEquals(one.hashCode(), one.getNetworkHandle());
+        assertNotEquals(two.hashCode(), two.getNetworkHandle());
+        assertNotEquals(three.hashCode(), three.getNetworkHandle());
+
+        // Adjust as necessary to test an implementation's specific constants.
+        // When running with runtest, "adb logcat -s TestRunner" can be useful.
+        assertEquals(7700664333L, one.getNetworkHandle());
+        assertEquals(11995631629L, two.getNetworkHandle());
+        assertEquals(16290598925L, three.getNetworkHandle());
+    }
+
+    @Test
+    public void testGetPrivateDnsBypassingCopy() {
+        final Network copy = mNetwork.getPrivateDnsBypassingCopy();
+        assertEquals(mNetwork.netId, copy.netId);
+        assertNotEquals(copy.netId, copy.getNetIdForResolv());
+        assertNotEquals(mNetwork.getNetIdForResolv(), copy.getNetIdForResolv());
+    }
+}
diff --git a/tests/common/java/android/net/OemNetworkPreferencesTest.java b/tests/common/java/android/net/OemNetworkPreferencesTest.java
new file mode 100644
index 0000000..fd29a95
--- /dev/null
+++ b/tests/common/java/android/net/OemNetworkPreferencesTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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 com.android.testutils.MiscAsserts.assertThrows;
+import static com.android.testutils.ParcelUtils.assertParcelSane;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+public class OemNetworkPreferencesTest {
+
+    private static final int TEST_PREF = OemNetworkPreferences.OEM_NETWORK_PREFERENCE_UNINITIALIZED;
+    private static final String TEST_PACKAGE = "com.google.apps.contacts";
+
+    private final OemNetworkPreferences.Builder mBuilder = new OemNetworkPreferences.Builder();
+
+    @Test
+    public void testBuilderAddNetworkPreferenceRequiresNonNullPackageName() {
+        assertThrows(NullPointerException.class,
+                () -> mBuilder.addNetworkPreference(null, TEST_PREF));
+    }
+
+    @Test
+    public void testBuilderRemoveNetworkPreferenceRequiresNonNullPackageName() {
+        assertThrows(NullPointerException.class,
+                () -> mBuilder.clearNetworkPreference(null));
+    }
+
+    @Test
+    public void testGetNetworkPreferenceReturnsCorrectValue() {
+        final int expectedNumberOfMappings = 1;
+        mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF);
+
+        final Map<String, Integer> networkPreferences =
+                mBuilder.build().getNetworkPreferences();
+
+        assertEquals(expectedNumberOfMappings, networkPreferences.size());
+        assertTrue(networkPreferences.containsKey(TEST_PACKAGE));
+    }
+
+    @Test
+    public void testGetNetworkPreferenceReturnsUnmodifiableValue() {
+        final String newPackage = "new.com.google.apps.contacts";
+        mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF);
+
+        final Map<String, Integer> networkPreferences =
+                mBuilder.build().getNetworkPreferences();
+
+        assertThrows(UnsupportedOperationException.class,
+                () -> networkPreferences.put(newPackage, TEST_PREF));
+
+        assertThrows(UnsupportedOperationException.class,
+                () -> networkPreferences.remove(TEST_PACKAGE));
+
+    }
+
+    @Test
+    public void testToStringReturnsCorrectValue() {
+        mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF);
+
+        final String networkPreferencesString = mBuilder.build().getNetworkPreferences().toString();
+
+        assertTrue(networkPreferencesString.contains(Integer.toString(TEST_PREF)));
+        assertTrue(networkPreferencesString.contains(TEST_PACKAGE));
+    }
+
+    @Test
+    public void testOemNetworkPreferencesParcelable() {
+        mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF);
+
+        final OemNetworkPreferences prefs = mBuilder.build();
+
+        assertParcelSane(prefs, 1 /* fieldCount */);
+    }
+
+    @Test
+    public void testAddNetworkPreferenceOverwritesPriorPreference() {
+        final int newPref = OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID;
+        mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF);
+        Map<String, Integer> networkPreferences =
+                mBuilder.build().getNetworkPreferences();
+
+        assertTrue(networkPreferences.containsKey(TEST_PACKAGE));
+        assertEquals(networkPreferences.get(TEST_PACKAGE).intValue(), TEST_PREF);
+
+        mBuilder.addNetworkPreference(TEST_PACKAGE, newPref);
+        networkPreferences = mBuilder.build().getNetworkPreferences();
+
+        assertTrue(networkPreferences.containsKey(TEST_PACKAGE));
+        assertEquals(networkPreferences.get(TEST_PACKAGE).intValue(), newPref);
+    }
+
+    @Test
+    public void testRemoveNetworkPreferenceRemovesValue() {
+        mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF);
+        Map<String, Integer> networkPreferences =
+                mBuilder.build().getNetworkPreferences();
+
+        assertTrue(networkPreferences.containsKey(TEST_PACKAGE));
+
+        mBuilder.clearNetworkPreference(TEST_PACKAGE);
+        networkPreferences = mBuilder.build().getNetworkPreferences();
+
+        assertFalse(networkPreferences.containsKey(TEST_PACKAGE));
+    }
+
+    @Test
+    public void testConstructorByOemNetworkPreferencesSetsValue() {
+        mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF);
+        OemNetworkPreferences networkPreference = mBuilder.build();
+
+        final Map<String, Integer> networkPreferences =
+                new OemNetworkPreferences
+                        .Builder(networkPreference)
+                        .build()
+                        .getNetworkPreferences();
+
+        assertTrue(networkPreferences.containsKey(TEST_PACKAGE));
+        assertEquals(networkPreferences.get(TEST_PACKAGE).intValue(), TEST_PREF);
+    }
+}
diff --git a/tests/common/java/android/net/RouteInfoTest.java b/tests/common/java/android/net/RouteInfoTest.java
new file mode 100644
index 0000000..71689f9
--- /dev/null
+++ b/tests/common/java/android/net/RouteInfoTest.java
@@ -0,0 +1,434 @@
+/*
+ * 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.
+ */
+
+package android.net;
+
+import static android.net.RouteInfo.RTN_UNREACHABLE;
+
+import static com.android.testutils.MiscAsserts.assertEqualBothWays;
+import static com.android.testutils.MiscAsserts.assertFieldCountEquals;
+import static com.android.testutils.MiscAsserts.assertNotEqualEitherWay;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.Build;
+
+import androidx.core.os.BuildCompat;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+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;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RouteInfoTest {
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+    private static final int INVALID_ROUTE_TYPE = -1;
+
+    private InetAddress Address(String addr) {
+        return InetAddresses.parseNumericAddress(addr);
+    }
+
+    private IpPrefix Prefix(String prefix) {
+        return new IpPrefix(prefix);
+    }
+
+    private static boolean isAtLeastR() {
+        // BuildCompat.isAtLeastR is documented to return false on release SDKs (including R)
+        return Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || BuildCompat.isAtLeastR();
+    }
+
+    @Test
+    public void testConstructor() {
+        RouteInfo r;
+        // Invalid input.
+        try {
+            r = new RouteInfo((IpPrefix) null, null, "rmnet0");
+            fail("Expected RuntimeException:  destination and gateway null");
+        } catch (RuntimeException e) { }
+
+        try {
+            r = new RouteInfo(Prefix("2001:db8:ace::/49"), Address("2001:db8::1"), "rmnet0",
+                    INVALID_ROUTE_TYPE);
+            fail("Invalid route type should cause exception");
+        } catch (IllegalArgumentException e) { }
+
+        try {
+            r = new RouteInfo(Prefix("2001:db8:ace::/49"), Address("192.0.2.1"), "rmnet0",
+                    RTN_UNREACHABLE);
+            fail("Address family mismatch should cause exception");
+        } catch (IllegalArgumentException e) { }
+
+        try {
+            r = new RouteInfo(Prefix("0.0.0.0/0"), Address("2001:db8::1"), "rmnet0",
+                    RTN_UNREACHABLE);
+            fail("Address family mismatch should cause exception");
+        } catch (IllegalArgumentException e) { }
+
+        // Null destination is default route.
+        r = new RouteInfo((IpPrefix) null, Address("2001:db8::1"), null);
+        assertEquals(Prefix("::/0"), r.getDestination());
+        assertEquals(Address("2001:db8::1"), r.getGateway());
+        assertNull(r.getInterface());
+
+        r = new RouteInfo((IpPrefix) null, Address("192.0.2.1"), "wlan0");
+        assertEquals(Prefix("0.0.0.0/0"), r.getDestination());
+        assertEquals(Address("192.0.2.1"), r.getGateway());
+        assertEquals("wlan0", r.getInterface());
+
+        // Null gateway sets gateway to unspecified address (why?).
+        r = new RouteInfo(Prefix("2001:db8:beef:cafe::/48"), null, "lo");
+        assertEquals(Prefix("2001:db8:beef::/48"), r.getDestination());
+        assertEquals(Address("::"), r.getGateway());
+        assertEquals("lo", r.getInterface());
+
+        r = new RouteInfo(Prefix("192.0.2.5/24"), null);
+        assertEquals(Prefix("192.0.2.0/24"), r.getDestination());
+        assertEquals(Address("0.0.0.0"), r.getGateway());
+        assertNull(r.getInterface());
+    }
+
+    @Test
+    public void testMatches() {
+        class PatchedRouteInfo {
+            private final RouteInfo mRouteInfo;
+
+            public PatchedRouteInfo(IpPrefix destination, InetAddress gateway, String iface) {
+                mRouteInfo = new RouteInfo(destination, gateway, iface);
+            }
+
+            public boolean matches(InetAddress destination) {
+                return mRouteInfo.matches(destination);
+            }
+        }
+
+        PatchedRouteInfo r;
+
+        r = new PatchedRouteInfo(Prefix("2001:db8:f00::ace:d00d/127"), null, "rmnet0");
+        assertTrue(r.matches(Address("2001:db8:f00::ace:d00c")));
+        assertTrue(r.matches(Address("2001:db8:f00::ace:d00d")));
+        assertFalse(r.matches(Address("2001:db8:f00::ace:d00e")));
+        assertFalse(r.matches(Address("2001:db8:f00::bad:d00d")));
+        assertFalse(r.matches(Address("2001:4868:4860::8888")));
+        assertFalse(r.matches(Address("8.8.8.8")));
+
+        r = new PatchedRouteInfo(Prefix("192.0.2.0/23"), null, "wlan0");
+        assertTrue(r.matches(Address("192.0.2.43")));
+        assertTrue(r.matches(Address("192.0.3.21")));
+        assertFalse(r.matches(Address("192.0.0.21")));
+        assertFalse(r.matches(Address("8.8.8.8")));
+
+        PatchedRouteInfo ipv6Default = new PatchedRouteInfo(Prefix("::/0"), null, "rmnet0");
+        assertTrue(ipv6Default.matches(Address("2001:db8::f00")));
+        assertFalse(ipv6Default.matches(Address("192.0.2.1")));
+
+        PatchedRouteInfo ipv4Default = new PatchedRouteInfo(Prefix("0.0.0.0/0"), null, "rmnet0");
+        assertTrue(ipv4Default.matches(Address("255.255.255.255")));
+        assertTrue(ipv4Default.matches(Address("192.0.2.1")));
+        assertFalse(ipv4Default.matches(Address("2001:db8::f00")));
+    }
+
+    @Test
+    public void testEquals() {
+        // IPv4
+        RouteInfo r1 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::1"), "wlan0");
+        RouteInfo r2 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::1"), "wlan0");
+        assertEqualBothWays(r1, r2);
+
+        RouteInfo r3 = new RouteInfo(Prefix("2001:db8:ace::/49"), Address("2001:db8::1"), "wlan0");
+        RouteInfo r4 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::2"), "wlan0");
+        RouteInfo r5 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::1"), "rmnet0");
+        assertNotEqualEitherWay(r1, r3);
+        assertNotEqualEitherWay(r1, r4);
+        assertNotEqualEitherWay(r1, r5);
+
+        // IPv6
+        r1 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), "wlan0");
+        r2 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), "wlan0");
+        assertEqualBothWays(r1, r2);
+
+        r3 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0");
+        r4 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.2"), "wlan0");
+        r5 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), "rmnet0");
+        assertNotEqualEitherWay(r1, r3);
+        assertNotEqualEitherWay(r1, r4);
+        assertNotEqualEitherWay(r1, r5);
+
+        // Interfaces (but not destinations or gateways) can be null.
+        r1 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), null);
+        r2 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), null);
+        r3 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0");
+        assertEqualBothWays(r1, r2);
+        assertNotEqualEitherWay(r1, r3);
+    }
+
+    @Test
+    public void testHostAndDefaultRoutes() {
+        RouteInfo r;
+
+        r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0");
+        assertFalse(r.isHostRoute());
+        assertTrue(r.isDefaultRoute());
+        assertTrue(r.isIPv4Default());
+        assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
+
+        r = new RouteInfo(Prefix("::/0"), Address("::"), "wlan0");
+        assertFalse(r.isHostRoute());
+        assertTrue(r.isDefaultRoute());
+        assertFalse(r.isIPv4Default());
+        assertTrue(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
+
+        r = new RouteInfo(Prefix("192.0.2.0/24"), null, "wlan0");
+        assertFalse(r.isHostRoute());
+        assertFalse(r.isDefaultRoute());
+        assertFalse(r.isIPv4Default());
+        assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
+
+        r = new RouteInfo(Prefix("2001:db8::/48"), null, "wlan0");
+        assertFalse(r.isHostRoute());
+        assertFalse(r.isDefaultRoute());
+        assertFalse(r.isIPv4Default());
+        assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
+
+        r = new RouteInfo(Prefix("192.0.2.0/32"), Address("0.0.0.0"), "wlan0");
+        assertTrue(r.isHostRoute());
+        assertFalse(r.isDefaultRoute());
+        assertFalse(r.isIPv4Default());
+        assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
+
+        r = new RouteInfo(Prefix("2001:db8::/128"), Address("::"), "wlan0");
+        assertTrue(r.isHostRoute());
+        assertFalse(r.isDefaultRoute());
+        assertFalse(r.isIPv4Default());
+        assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
+
+        r = new RouteInfo(Prefix("192.0.2.0/32"), null, "wlan0");
+        assertTrue(r.isHostRoute());
+        assertFalse(r.isDefaultRoute());
+        assertFalse(r.isIPv4Default());
+        assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
+
+        r = new RouteInfo(Prefix("2001:db8::/128"), null, "wlan0");
+        assertTrue(r.isHostRoute());
+        assertFalse(r.isDefaultRoute());
+        assertFalse(r.isIPv4Default());
+        assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
+
+        r = new RouteInfo(Prefix("::/128"), Address("fe80::"), "wlan0");
+        assertTrue(r.isHostRoute());
+        assertFalse(r.isDefaultRoute());
+        assertFalse(r.isIPv4Default());
+        assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
+
+        r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0");
+        assertTrue(r.isHostRoute());
+        assertFalse(r.isDefaultRoute());
+        assertFalse(r.isIPv4Default());
+        assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
+
+        r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0");
+        assertTrue(r.isHostRoute());
+        assertFalse(r.isDefaultRoute());
+        assertFalse(r.isIPv4Default());
+        assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
+
+        r = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE);
+        assertFalse(r.isHostRoute());
+        assertFalse(r.isDefaultRoute());
+        assertFalse(r.isIPv4Default());
+        assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertTrue(r.isIPv4UnreachableDefault());
+            assertFalse(r.isIPv6UnreachableDefault());
+        }
+
+        r = new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE);
+        assertFalse(r.isHostRoute());
+        assertFalse(r.isDefaultRoute());
+        assertFalse(r.isIPv4Default());
+        assertFalse(r.isIPv6Default());
+        if (isAtLeastR()) {
+            assertFalse(r.isIPv4UnreachableDefault());
+            assertTrue(r.isIPv6UnreachableDefault());
+        }
+    }
+
+    @Test
+    public void testTruncation() {
+      LinkAddress l;
+      RouteInfo r;
+
+      l = new LinkAddress("192.0.2.5/30");
+      r = new RouteInfo(l, Address("192.0.2.1"), "wlan0");
+      assertEquals("192.0.2.4", r.getDestination().getAddress().getHostAddress());
+
+      l = new LinkAddress("2001:db8:1:f::5/63");
+      r = new RouteInfo(l, Address("2001:db8:5::1"), "wlan0");
+      assertEquals("2001:db8:1:e::", r.getDestination().getAddress().getHostAddress());
+    }
+
+    // Make sure that creating routes to multicast addresses doesn't throw an exception. Even though
+    // there's nothing we can do with them, we don't want to crash if, e.g., someone calls
+    // requestRouteToHostAddress("230.0.0.0", MOBILE_HIPRI);
+    @Test
+    public void testMulticastRoute() {
+      RouteInfo r;
+      r = new RouteInfo(Prefix("230.0.0.0/32"), Address("192.0.2.1"), "wlan0");
+      r = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::1"), "wlan0");
+      // No exceptions? Good.
+    }
+
+    @Test
+    public void testParceling() {
+        RouteInfo r;
+        r = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), null);
+        assertParcelingIsLossless(r);
+        r = new RouteInfo(Prefix("192.0.2.0/24"), null, "wlan0");
+        assertParcelingIsLossless(r);
+        r = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0", RTN_UNREACHABLE);
+        assertParcelingIsLossless(r);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testMtuParceling() {
+        final RouteInfo r = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::"), "testiface",
+                RTN_UNREACHABLE, 1450 /* mtu */);
+        assertParcelingIsLossless(r);
+    }
+
+    @Test @IgnoreAfter(Build.VERSION_CODES.Q)
+    public void testFieldCount_Q() {
+        assertFieldCountEquals(6, RouteInfo.class);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testFieldCount() {
+        // Make sure any new field is covered by the above parceling tests when changing this number
+        assertFieldCountEquals(7, RouteInfo.class);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testMtu() {
+        RouteInfo r;
+        r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0",
+                RouteInfo.RTN_UNICAST, 1500);
+        assertEquals(1500, r.getMtu());
+
+        r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0");
+        assertEquals(0, r.getMtu());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testRouteKey() {
+        RouteInfo.RouteKey k1, k2;
+        // Only prefix, null gateway and null interface
+        k1 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey();
+        k2 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey();
+        assertEquals(k1, k2);
+        assertEquals(k1.hashCode(), k2.hashCode());
+
+        // With prefix, gateway and interface. Type and MTU does not affect RouteKey equality
+        k1 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0",
+                RTN_UNREACHABLE, 1450).getRouteKey();
+        k2 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0",
+                RouteInfo.RTN_UNICAST, 1400).getRouteKey();
+        assertEquals(k1, k2);
+        assertEquals(k1.hashCode(), k2.hashCode());
+
+        // Different scope IDs are ignored by the kernel, so we consider them equal here too.
+        k1 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%1"), "wlan0").getRouteKey();
+        k2 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%2"), "wlan0").getRouteKey();
+        assertEquals(k1, k2);
+        assertEquals(k1.hashCode(), k2.hashCode());
+
+        // Different prefix
+        k1 = new RouteInfo(Prefix("192.0.2.0/24"), null).getRouteKey();
+        k2 = new RouteInfo(Prefix("192.0.3.0/24"), null).getRouteKey();
+        assertNotEquals(k1, k2);
+
+        // Different gateway
+        k1 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::1"), null).getRouteKey();
+        k2 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::2"), null).getRouteKey();
+        assertNotEquals(k1, k2);
+
+        // Different interface
+        k1 = new RouteInfo(Prefix("ff02::1/128"), null, "tun0").getRouteKey();
+        k2 = new RouteInfo(Prefix("ff02::1/128"), null, "tun1").getRouteKey();
+        assertNotEquals(k1, k2);
+    }
+}
diff --git a/tests/common/java/android/net/StaticIpConfigurationTest.java b/tests/common/java/android/net/StaticIpConfigurationTest.java
new file mode 100644
index 0000000..b5f23bf
--- /dev/null
+++ b/tests/common/java/android/net/StaticIpConfigurationTest.java
@@ -0,0 +1,269 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StaticIpConfigurationTest {
+
+    private static final String ADDRSTR = "192.0.2.2/25";
+    private static final LinkAddress ADDR = new LinkAddress(ADDRSTR);
+    private static final InetAddress GATEWAY = IpAddress("192.0.2.1");
+    private static final InetAddress OFFLINKGATEWAY = IpAddress("192.0.2.129");
+    private static final InetAddress DNS1 = IpAddress("8.8.8.8");
+    private static final InetAddress DNS2 = IpAddress("8.8.4.4");
+    private static final InetAddress DNS3 = IpAddress("4.2.2.2");
+    private static final String IFACE = "eth0";
+    private static final String FAKE_DOMAINS = "google.com";
+
+    private static InetAddress IpAddress(String addr) {
+        return InetAddress.parseNumericAddress(addr);
+    }
+
+    private void checkEmpty(StaticIpConfiguration s) {
+        assertNull(s.ipAddress);
+        assertNull(s.gateway);
+        assertNull(s.domains);
+        assertEquals(0, s.dnsServers.size());
+    }
+
+    private StaticIpConfiguration makeTestObject() {
+        StaticIpConfiguration s = new StaticIpConfiguration();
+        s.ipAddress = ADDR;
+        s.gateway = GATEWAY;
+        s.dnsServers.add(DNS1);
+        s.dnsServers.add(DNS2);
+        s.dnsServers.add(DNS3);
+        s.domains = FAKE_DOMAINS;
+        return s;
+    }
+
+    @Test
+    public void testConstructor() {
+        StaticIpConfiguration s = new StaticIpConfiguration();
+        checkEmpty(s);
+    }
+
+    @Test
+    public void testCopyAndClear() {
+        StaticIpConfiguration empty = new StaticIpConfiguration((StaticIpConfiguration) null);
+        checkEmpty(empty);
+
+        StaticIpConfiguration s1 = makeTestObject();
+        StaticIpConfiguration s2 = new StaticIpConfiguration(s1);
+        assertEquals(s1, s2);
+        s2.clear();
+        assertEquals(empty, s2);
+    }
+
+    @Test
+    public void testHashCodeAndEquals() {
+        HashSet<Integer> hashCodes = new HashSet();
+        hashCodes.add(0);
+
+        StaticIpConfiguration s = new StaticIpConfiguration();
+        // Check that this hash code is nonzero and different from all the ones seen so far.
+        assertTrue(hashCodes.add(s.hashCode()));
+
+        s.ipAddress = ADDR;
+        assertTrue(hashCodes.add(s.hashCode()));
+
+        s.gateway = GATEWAY;
+        assertTrue(hashCodes.add(s.hashCode()));
+
+        s.dnsServers.add(DNS1);
+        assertTrue(hashCodes.add(s.hashCode()));
+
+        s.dnsServers.add(DNS2);
+        assertTrue(hashCodes.add(s.hashCode()));
+
+        s.dnsServers.add(DNS3);
+        assertTrue(hashCodes.add(s.hashCode()));
+
+        s.domains = "example.com";
+        assertTrue(hashCodes.add(s.hashCode()));
+
+        assertFalse(s.equals(null));
+        assertEquals(s, s);
+
+        StaticIpConfiguration s2 = new StaticIpConfiguration(s);
+        assertEquals(s, s2);
+
+        s.ipAddress = new LinkAddress(DNS1, 32);
+        assertNotEquals(s, s2);
+
+        s2 = new StaticIpConfiguration(s);
+        s.domains = "foo";
+        assertNotEquals(s, s2);
+
+        s2 = new StaticIpConfiguration(s);
+        s.gateway = DNS2;
+        assertNotEquals(s, s2);
+
+        s2 = new StaticIpConfiguration(s);
+        s.dnsServers.add(DNS3);
+        assertNotEquals(s, s2);
+    }
+
+    @Test
+    public void testToLinkProperties() {
+        LinkProperties expected = new LinkProperties();
+        expected.setInterfaceName(IFACE);
+
+        StaticIpConfiguration s = new StaticIpConfiguration();
+        assertEquals(expected, s.toLinkProperties(IFACE));
+
+        final RouteInfo connectedRoute = new RouteInfo(new IpPrefix(ADDRSTR), null, IFACE);
+        s.ipAddress = ADDR;
+        expected.addLinkAddress(ADDR);
+        expected.addRoute(connectedRoute);
+        assertEquals(expected, s.toLinkProperties(IFACE));
+
+        s.gateway = GATEWAY;
+        RouteInfo defaultRoute = new RouteInfo(new IpPrefix("0.0.0.0/0"), GATEWAY, IFACE);
+        expected.addRoute(defaultRoute);
+        assertEquals(expected, s.toLinkProperties(IFACE));
+
+        s.gateway = OFFLINKGATEWAY;
+        expected.removeRoute(defaultRoute);
+        defaultRoute = new RouteInfo(new IpPrefix("0.0.0.0/0"), OFFLINKGATEWAY, IFACE);
+        expected.addRoute(defaultRoute);
+
+        RouteInfo gatewayRoute = new RouteInfo(new IpPrefix("192.0.2.129/32"), null, IFACE);
+        expected.addRoute(gatewayRoute);
+        assertEquals(expected, s.toLinkProperties(IFACE));
+
+        s.dnsServers.add(DNS1);
+        expected.addDnsServer(DNS1);
+        assertEquals(expected, s.toLinkProperties(IFACE));
+
+        s.dnsServers.add(DNS2);
+        s.dnsServers.add(DNS3);
+        expected.addDnsServer(DNS2);
+        expected.addDnsServer(DNS3);
+        assertEquals(expected, s.toLinkProperties(IFACE));
+
+        s.domains = FAKE_DOMAINS;
+        expected.setDomains(FAKE_DOMAINS);
+        assertEquals(expected, s.toLinkProperties(IFACE));
+
+        s.gateway = null;
+        expected.removeRoute(defaultRoute);
+        expected.removeRoute(gatewayRoute);
+        assertEquals(expected, s.toLinkProperties(IFACE));
+
+        // Without knowing the IP address, we don't have a directly-connected route, so we can't
+        // tell if the gateway is off-link or not and we don't add a host route. This isn't a real
+        // configuration, but we should at least not crash.
+        s.gateway = OFFLINKGATEWAY;
+        s.ipAddress = null;
+        expected.removeLinkAddress(ADDR);
+        expected.removeRoute(connectedRoute);
+        expected.addRoute(defaultRoute);
+        assertEquals(expected, s.toLinkProperties(IFACE));
+    }
+
+    private StaticIpConfiguration passThroughParcel(StaticIpConfiguration s) {
+        Parcel p = Parcel.obtain();
+        StaticIpConfiguration s2 = null;
+        try {
+            s.writeToParcel(p, 0);
+            p.setDataPosition(0);
+            s2 = StaticIpConfiguration.readFromParcel(p);
+        } finally {
+            p.recycle();
+        }
+        assertNotNull(s2);
+        return s2;
+    }
+
+    @Test
+    public void testParceling() {
+        StaticIpConfiguration s = makeTestObject();
+        StaticIpConfiguration s2 = passThroughParcel(s);
+        assertEquals(s, s2);
+    }
+
+    @Test
+    public void testBuilder() {
+        final ArrayList<InetAddress> dnsServers = new ArrayList<>();
+        dnsServers.add(DNS1);
+
+        final StaticIpConfiguration s = new StaticIpConfiguration.Builder()
+                .setIpAddress(ADDR)
+                .setGateway(GATEWAY)
+                .setDomains(FAKE_DOMAINS)
+                .setDnsServers(dnsServers)
+                .build();
+
+        assertEquals(s.ipAddress, s.getIpAddress());
+        assertEquals(ADDR, s.getIpAddress());
+        assertEquals(s.gateway, s.getGateway());
+        assertEquals(GATEWAY, s.getGateway());
+        assertEquals(s.domains, s.getDomains());
+        assertEquals(FAKE_DOMAINS, s.getDomains());
+        assertTrue(s.dnsServers.equals(s.getDnsServers()));
+        assertEquals(1, s.getDnsServers().size());
+        assertEquals(DNS1, s.getDnsServers().get(0));
+    }
+
+    @Test
+    public void testAddDnsServers() {
+        final StaticIpConfiguration s = new StaticIpConfiguration((StaticIpConfiguration) null);
+        checkEmpty(s);
+
+        s.addDnsServer(DNS1);
+        assertEquals(1, s.getDnsServers().size());
+        assertEquals(DNS1, s.getDnsServers().get(0));
+
+        s.addDnsServer(DNS2);
+        s.addDnsServer(DNS3);
+        assertEquals(3, s.getDnsServers().size());
+        assertEquals(DNS2, s.getDnsServers().get(1));
+        assertEquals(DNS3, s.getDnsServers().get(2));
+    }
+
+    @Test
+    public void testGetRoutes() {
+        final StaticIpConfiguration s = makeTestObject();
+        final List<RouteInfo> routeInfoList = s.getRoutes(IFACE);
+
+        assertEquals(2, routeInfoList.size());
+        assertEquals(new RouteInfo(ADDR, (InetAddress) null, IFACE), routeInfoList.get(0));
+        assertEquals(new RouteInfo((IpPrefix) null, GATEWAY, IFACE), routeInfoList.get(1));
+    }
+}
diff --git a/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt b/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt
new file mode 100644
index 0000000..7a18bb0
--- /dev/null
+++ b/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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
+
+import android.net.InetAddresses.parseNumericAddress
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertFieldCountEquals
+import com.android.testutils.assertParcelSane
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.InetAddress
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) // TcpKeepalivePacketData added to SDK in S
+class TcpKeepalivePacketDataTest {
+    private fun makeData(
+        srcAddress: InetAddress = parseNumericAddress("192.0.2.123"),
+        srcPort: Int = 1234,
+        dstAddress: InetAddress = parseNumericAddress("192.0.2.231"),
+        dstPort: Int = 4321,
+        data: ByteArray = byteArrayOf(1, 2, 3),
+        tcpSeq: Int = 135,
+        tcpAck: Int = 246,
+        tcpWnd: Int = 1234,
+        tcpWndScale: Int = 2,
+        ipTos: Int = 0x12,
+        ipTtl: Int = 10
+    ) = TcpKeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, data, tcpSeq, tcpAck,
+            tcpWnd, tcpWndScale, ipTos, ipTtl)
+
+    @Test
+    fun testEquals() {
+        val data1 = makeData()
+        val data2 = makeData()
+        assertEquals(data1, data2)
+        assertEquals(data1.hashCode(), data2.hashCode())
+    }
+
+    @Test
+    fun testNotEquals() {
+        assertNotEquals(makeData(srcAddress = parseNumericAddress("192.0.2.124")), makeData())
+        assertNotEquals(makeData(srcPort = 1235), makeData())
+        assertNotEquals(makeData(dstAddress = parseNumericAddress("192.0.2.232")), makeData())
+        assertNotEquals(makeData(dstPort = 4322), makeData())
+        // .equals does not test .packet, as it should be generated from the other fields
+        assertNotEquals(makeData(tcpSeq = 136), makeData())
+        assertNotEquals(makeData(tcpAck = 247), makeData())
+        assertNotEquals(makeData(tcpWnd = 1235), makeData())
+        assertNotEquals(makeData(tcpWndScale = 3), makeData())
+        assertNotEquals(makeData(ipTos = 0x14), makeData())
+        assertNotEquals(makeData(ipTtl = 11), makeData())
+
+        // Update above assertions if field is added
+        assertFieldCountEquals(5, KeepalivePacketData::class.java)
+        assertFieldCountEquals(6, TcpKeepalivePacketData::class.java)
+    }
+
+    @Test
+    fun testParcelUnparcel() {
+        assertParcelSane(makeData(), fieldCount = 6) { a, b ->
+            // .equals() does not verify .packet
+            a == b && a.packet contentEquals b.packet
+        }
+    }
+
+    @Test
+    fun testToString() {
+        val data = makeData()
+        val str = data.toString()
+
+        assertTrue(str.contains(data.srcAddress.hostAddress))
+        assertTrue(str.contains(data.srcPort.toString()))
+        assertTrue(str.contains(data.dstAddress.hostAddress))
+        assertTrue(str.contains(data.dstPort.toString()))
+        // .packet not included in toString()
+        assertTrue(str.contains(data.getTcpSeq().toString()))
+        assertTrue(str.contains(data.getTcpAck().toString()))
+        assertTrue(str.contains(data.getTcpWindow().toString()))
+        assertTrue(str.contains(data.getTcpWindowScale().toString()))
+        assertTrue(str.contains(data.getIpTos().toString()))
+        assertTrue(str.contains(data.getIpTtl().toString()))
+
+        // Update above assertions if field is added
+        assertFieldCountEquals(5, KeepalivePacketData::class.java)
+        assertFieldCountEquals(6, TcpKeepalivePacketData::class.java)
+    }
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/UidRangeTest.java b/tests/common/java/android/net/UidRangeTest.java
new file mode 100644
index 0000000..1b1c954
--- /dev/null
+++ b/tests/common/java/android/net/UidRangeTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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 static android.os.UserHandle.MIN_SECONDARY_USER_ID;
+import static android.os.UserHandle.SYSTEM;
+import static android.os.UserHandle.USER_SYSTEM;
+import static android.os.UserHandle.getUid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.Build;
+import android.os.UserHandle;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UidRangeTest {
+
+    /*
+     * UidRange is no longer passed to netd. UID ranges between the framework and netd are passed as
+     * UidRangeParcel objects.
+     */
+
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+    @Test
+    public void testSingleItemUidRangeAllowed() {
+        new UidRange(123, 123);
+        new UidRange(0, 0);
+        new UidRange(Integer.MAX_VALUE, Integer.MAX_VALUE);
+    }
+
+    @Test
+    public void testNegativeUidsDisallowed() {
+        try {
+            new UidRange(-2, 100);
+            fail("Exception not thrown for negative start UID");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        try {
+            new UidRange(-200, -100);
+            fail("Exception not thrown for negative stop UID");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testStopLessThanStartDisallowed() {
+        final int x = 4195000;
+        try {
+            new UidRange(x, x - 1);
+            fail("Exception not thrown for negative-length UID range");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testGetStartAndEndUser() throws Exception {
+        final UidRange uidRangeOfPrimaryUser = new UidRange(
+                getUid(USER_SYSTEM, 10000), getUid(USER_SYSTEM, 10100));
+        final UidRange uidRangeOfSecondaryUser = new UidRange(
+                getUid(MIN_SECONDARY_USER_ID, 10000), getUid(MIN_SECONDARY_USER_ID, 10100));
+        assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getStartUser());
+        assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getEndUser());
+        assertEquals(MIN_SECONDARY_USER_ID, uidRangeOfSecondaryUser.getStartUser());
+        assertEquals(MIN_SECONDARY_USER_ID, uidRangeOfSecondaryUser.getEndUser());
+
+        final UidRange uidRangeForDifferentUsers = new UidRange(
+                getUid(USER_SYSTEM, 10000), getUid(MIN_SECONDARY_USER_ID, 10100));
+        assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getStartUser());
+        assertEquals(MIN_SECONDARY_USER_ID, uidRangeOfSecondaryUser.getEndUser());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testCreateForUser() throws Exception {
+        final UidRange uidRangeOfPrimaryUser = UidRange.createForUser(SYSTEM);
+        final UidRange uidRangeOfSecondaryUser = UidRange.createForUser(
+                UserHandle.of(USER_SYSTEM + 1));
+        assertTrue(uidRangeOfPrimaryUser.stop < uidRangeOfSecondaryUser.start);
+        assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getStartUser());
+        assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getEndUser());
+        assertEquals(USER_SYSTEM + 1, uidRangeOfSecondaryUser.getStartUser());
+        assertEquals(USER_SYSTEM + 1, uidRangeOfSecondaryUser.getEndUser());
+    }
+}
diff --git a/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt b/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt
new file mode 100644
index 0000000..f23ba26
--- /dev/null
+++ b/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelSane
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+
+private const val TEST_OWNER_UID = 123
+private const val TEST_IFACE = "test_tun0"
+private val TEST_IFACE_LIST = listOf("wlan0", "rmnet_data0", "eth0")
+
+@SmallTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class UnderlyingNetworkInfoTest {
+    @Test
+    fun testParcelUnparcel() {
+        val testInfo = UnderlyingNetworkInfo(TEST_OWNER_UID, TEST_IFACE, TEST_IFACE_LIST)
+        assertEquals(TEST_OWNER_UID, testInfo.getOwnerUid())
+        assertEquals(TEST_IFACE, testInfo.getInterface())
+        assertEquals(TEST_IFACE_LIST, testInfo.getUnderlyingInterfaces())
+        assertParcelSane(testInfo, 3)
+
+        val emptyInfo = UnderlyingNetworkInfo(0, String(), listOf())
+        assertEquals(0, emptyInfo.getOwnerUid())
+        assertEquals(String(), emptyInfo.getInterface())
+        assertEquals(listOf(), emptyInfo.getUnderlyingInterfaces())
+        assertParcelSane(emptyInfo, 3)
+    }
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/apf/ApfCapabilitiesTest.java b/tests/common/java/android/net/apf/ApfCapabilitiesTest.java
new file mode 100644
index 0000000..88996d9
--- /dev/null
+++ b/tests/common/java/android/net/apf/ApfCapabilitiesTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.apf;
+
+import static com.android.testutils.ParcelUtils.assertParcelSane;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ApfCapabilitiesTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getContext();
+    }
+
+    @Test
+    public void testConstructAndParcel() {
+        final ApfCapabilities caps = new ApfCapabilities(123, 456, 789);
+        assertEquals(123, caps.apfVersionSupported);
+        assertEquals(456, caps.maximumApfProgramSize);
+        assertEquals(789, caps.apfPacketFormat);
+
+        assertParcelSane(caps, 3);
+    }
+
+    @Test
+    public void testEquals() {
+        assertEquals(new ApfCapabilities(1, 2, 3), new ApfCapabilities(1, 2, 3));
+        assertNotEquals(new ApfCapabilities(2, 2, 3), new ApfCapabilities(1, 2, 3));
+        assertNotEquals(new ApfCapabilities(1, 3, 3), new ApfCapabilities(1, 2, 3));
+        assertNotEquals(new ApfCapabilities(1, 2, 4), new ApfCapabilities(1, 2, 3));
+    }
+
+    @Test
+    public void testHasDataAccess() {
+        //hasDataAccess is only supported starting at apf version 4.
+        ApfCapabilities caps = new ApfCapabilities(1 /* apfVersionSupported */, 2, 3);
+        assertFalse(caps.hasDataAccess());
+
+        caps = new ApfCapabilities(4 /* apfVersionSupported */, 5, 6);
+        assertTrue(caps.hasDataAccess());
+    }
+
+    @Test
+    public void testGetApfDrop8023Frames() {
+        // Get com.android.internal.R.bool.config_apfDrop802_3Frames. The test cannot directly
+        // use R.bool.config_apfDrop802_3Frames because that is not a stable resource ID.
+        final int resId = mContext.getResources().getIdentifier("config_apfDrop802_3Frames",
+                "bool", "android");
+        final boolean shouldDrop8023Frames = mContext.getResources().getBoolean(resId);
+        final boolean actual = ApfCapabilities.getApfDrop8023Frames();
+        assertEquals(shouldDrop8023Frames, actual);
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testGetApfDrop8023Frames_S() {
+        // IpClient does not call getApfDrop8023Frames() since S, so any customization of the return
+        // value on S+ is a configuration error as it will not be used by IpClient.
+        assertTrue("android.R.bool.config_apfDrop802_3Frames has been modified to false, but "
+                + "starting from S its value is not used by IpClient. If the modification is "
+                + "intentional, use a runtime resource overlay for the NetworkStack package to "
+                + "overlay com.android.networkstack.R.bool.config_apfDrop802_3Frames instead.",
+                ApfCapabilities.getApfDrop8023Frames());
+    }
+
+    @Test
+    public void testGetApfEtherTypeBlackList() {
+        // Get com.android.internal.R.array.config_apfEthTypeBlackList. The test cannot directly
+        // use R.array.config_apfEthTypeBlackList because that is not a stable resource ID.
+        final int resId = mContext.getResources().getIdentifier("config_apfEthTypeBlackList",
+                "array", "android");
+        final int[] blacklistedEtherTypeArray = mContext.getResources().getIntArray(resId);
+        final int[] actual = ApfCapabilities.getApfEtherTypeBlackList();
+        assertNotNull(actual);
+        assertTrue(Arrays.equals(blacklistedEtherTypeArray, actual));
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testGetApfEtherTypeBlackList_S() {
+        // IpClient does not call getApfEtherTypeBlackList() since S, so any customization of the
+        // return value on S+ is a configuration error as it will not be used by IpClient.
+        assertArrayEquals("android.R.array.config_apfEthTypeBlackList has been modified, but "
+                        + "starting from S its value is not used by IpClient. If the modification "
+                        + "is intentional, use a runtime resource overlay for the NetworkStack "
+                        + "package to overlay "
+                        + "com.android.networkstack.R.array.config_apfEthTypeDenyList instead.",
+                new int[] { 0x88a2, 0x88a4, 0x88b8, 0x88cd, 0x88e3 },
+                ApfCapabilities.getApfEtherTypeBlackList());
+    }
+}
diff --git a/tests/common/java/android/net/metrics/ApfProgramEventTest.kt b/tests/common/java/android/net/metrics/ApfProgramEventTest.kt
new file mode 100644
index 0000000..0b7b740
--- /dev/null
+++ b/tests/common/java/android/net/metrics/ApfProgramEventTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.metrics;
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.assertParcelSane
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ApfProgramEventTest {
+    private infix fun Int.hasFlag(flag: Int) = (this and (1 shl flag)) != 0
+
+    @Test
+    fun testBuilderAndParcel() {
+        val apfProgramEvent = ApfProgramEvent.Builder()
+                .setLifetime(1)
+                .setActualLifetime(2)
+                .setFilteredRas(3)
+                .setCurrentRas(4)
+                .setProgramLength(5)
+                .setFlags(true, true)
+                .build()
+
+        assertEquals(1, apfProgramEvent.lifetime)
+        assertEquals(2, apfProgramEvent.actualLifetime)
+        assertEquals(3, apfProgramEvent.filteredRas)
+        assertEquals(4, apfProgramEvent.currentRas)
+        assertEquals(5, apfProgramEvent.programLength)
+        assertEquals(ApfProgramEvent.flagsFor(true, true), apfProgramEvent.flags)
+
+        assertParcelSane(apfProgramEvent, 6)
+    }
+
+    @Test
+    fun testFlagsFor() {
+        var flags = ApfProgramEvent.flagsFor(false, false)
+        assertFalse(flags hasFlag ApfProgramEvent.FLAG_HAS_IPV4_ADDRESS)
+        assertFalse(flags hasFlag ApfProgramEvent.FLAG_MULTICAST_FILTER_ON)
+
+        flags = ApfProgramEvent.flagsFor(true, false)
+        assertTrue(flags hasFlag ApfProgramEvent.FLAG_HAS_IPV4_ADDRESS)
+        assertFalse(flags hasFlag ApfProgramEvent.FLAG_MULTICAST_FILTER_ON)
+
+        flags = ApfProgramEvent.flagsFor(false, true)
+        assertFalse(flags hasFlag ApfProgramEvent.FLAG_HAS_IPV4_ADDRESS)
+        assertTrue(flags hasFlag ApfProgramEvent.FLAG_MULTICAST_FILTER_ON)
+
+        flags = ApfProgramEvent.flagsFor(true, true)
+        assertTrue(flags hasFlag ApfProgramEvent.FLAG_HAS_IPV4_ADDRESS)
+        assertTrue(flags hasFlag ApfProgramEvent.FLAG_MULTICAST_FILTER_ON)
+    }
+}
diff --git a/tests/common/java/android/net/metrics/ApfStatsTest.kt b/tests/common/java/android/net/metrics/ApfStatsTest.kt
new file mode 100644
index 0000000..46a8c8e
--- /dev/null
+++ b/tests/common/java/android/net/metrics/ApfStatsTest.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.metrics
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.assertParcelSane
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ApfStatsTest {
+    @Test
+    fun testBuilderAndParcel() {
+        val apfStats = ApfStats.Builder()
+                .setDurationMs(Long.MAX_VALUE)
+                .setReceivedRas(1)
+                .setMatchingRas(2)
+                .setDroppedRas(3)
+                .setZeroLifetimeRas(4)
+                .setParseErrors(5)
+                .setProgramUpdates(6)
+                .setProgramUpdatesAll(7)
+                .setProgramUpdatesAllowingMulticast(8)
+                .setMaxProgramSize(9)
+                .build()
+
+        assertEquals(Long.MAX_VALUE, apfStats.durationMs)
+        assertEquals(1, apfStats.receivedRas)
+        assertEquals(2, apfStats.matchingRas)
+        assertEquals(3, apfStats.droppedRas)
+        assertEquals(4, apfStats.zeroLifetimeRas)
+        assertEquals(5, apfStats.parseErrors)
+        assertEquals(6, apfStats.programUpdates)
+        assertEquals(7, apfStats.programUpdatesAll)
+        assertEquals(8, apfStats.programUpdatesAllowingMulticast)
+        assertEquals(9, apfStats.maxProgramSize)
+
+        assertParcelSane(apfStats, 10)
+    }
+}
diff --git a/tests/common/java/android/net/metrics/DhcpClientEventTest.kt b/tests/common/java/android/net/metrics/DhcpClientEventTest.kt
new file mode 100644
index 0000000..8d7a9c4
--- /dev/null
+++ b/tests/common/java/android/net/metrics/DhcpClientEventTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.metrics
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.assertParcelSane
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val FAKE_MESSAGE = "test"
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class DhcpClientEventTest {
+    @Test
+    fun testBuilderAndParcel() {
+        val dhcpClientEvent = DhcpClientEvent.Builder()
+                .setMsg(FAKE_MESSAGE)
+                .setDurationMs(Integer.MAX_VALUE)
+                .build()
+
+        assertEquals(FAKE_MESSAGE, dhcpClientEvent.msg)
+        assertEquals(Integer.MAX_VALUE, dhcpClientEvent.durationMs)
+
+        assertParcelSane(dhcpClientEvent, 2)
+    }
+}
diff --git a/tests/common/java/android/net/metrics/DhcpErrorEventTest.kt b/tests/common/java/android/net/metrics/DhcpErrorEventTest.kt
new file mode 100644
index 0000000..236f72e
--- /dev/null
+++ b/tests/common/java/android/net/metrics/DhcpErrorEventTest.kt
@@ -0,0 +1,65 @@
+package android.net.metrics
+
+import android.net.metrics.DhcpErrorEvent.DHCP_INVALID_OPTION_LENGTH
+import android.net.metrics.DhcpErrorEvent.errorCodeWithOption
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.parcelingRoundTrip
+import java.lang.reflect.Modifier
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_ERROR_CODE = 12345
+//DHCP Optional Type: DHCP Subnet Mask (Copy from DhcpPacket.java due to it's protected)
+private const val DHCP_SUBNET_MASK = 1
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class DhcpErrorEventTest {
+
+    @Test
+    fun testConstructor() {
+        val event = DhcpErrorEvent(TEST_ERROR_CODE)
+        assertEquals(TEST_ERROR_CODE, event.errorCode)
+    }
+
+    @Test
+    fun testParcelUnparcel() {
+        val event = DhcpErrorEvent(TEST_ERROR_CODE)
+        val parceled = parcelingRoundTrip(event)
+        assertEquals(TEST_ERROR_CODE, parceled.errorCode)
+    }
+
+    @Test
+    fun testErrorCodeWithOption() {
+        val errorCode = errorCodeWithOption(DHCP_INVALID_OPTION_LENGTH, DHCP_SUBNET_MASK);
+        assertTrue((DHCP_INVALID_OPTION_LENGTH and errorCode) == DHCP_INVALID_OPTION_LENGTH);
+        assertTrue((DHCP_SUBNET_MASK and errorCode) == DHCP_SUBNET_MASK);
+    }
+
+    @Test
+    fun testToString() {
+        val names = listOf("L2_ERROR", "L3_ERROR", "L4_ERROR", "DHCP_ERROR", "MISC_ERROR")
+        val errorFields = DhcpErrorEvent::class.java.declaredFields.filter {
+            it.type == Int::class.javaPrimitiveType
+                    && Modifier.isPublic(it.modifiers) && Modifier.isStatic(it.modifiers)
+                    && it.name !in names
+        }
+
+        errorFields.forEach {
+            val intValue = it.getInt(null)
+            val stringValue = DhcpErrorEvent(intValue).toString()
+            assertTrue("Invalid string for error 0x%08X (field %s): %s".format(intValue, it.name,
+                    stringValue),
+                    stringValue.contains(it.name))
+        }
+    }
+
+    @Test
+    fun testToString_InvalidErrorCode() {
+        assertNotNull(DhcpErrorEvent(TEST_ERROR_CODE).toString())
+    }
+}
diff --git a/tests/common/java/android/net/metrics/IpConnectivityLogTest.java b/tests/common/java/android/net/metrics/IpConnectivityLogTest.java
new file mode 100644
index 0000000..d4780d3
--- /dev/null
+++ b/tests/common/java/android/net/metrics/IpConnectivityLogTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.metrics;
+
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.net.ConnectivityMetricsEvent;
+import android.net.IIpConnectivityMetrics;
+import android.net.Network;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.BitUtils;
+
+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.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpConnectivityLogTest {
+    private static final int FAKE_NET_ID = 100;
+    private static final int[] FAKE_TRANSPORT_TYPES = BitUtils.unpackBits(TRANSPORT_WIFI);
+    private static final long FAKE_TIME_STAMP = System.currentTimeMillis();
+    private static final String FAKE_INTERFACE_NAME = "test";
+    private static final IpReachabilityEvent FAKE_EV =
+            new IpReachabilityEvent(IpReachabilityEvent.NUD_FAILED);
+
+    @Mock IIpConnectivityMetrics mMockService;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testLoggingEvents() throws Exception {
+        IpConnectivityLog logger = new IpConnectivityLog(mMockService);
+
+        assertTrue(logger.log(FAKE_EV));
+        assertTrue(logger.log(FAKE_TIME_STAMP, FAKE_EV));
+        assertTrue(logger.log(FAKE_NET_ID, FAKE_TRANSPORT_TYPES, FAKE_EV));
+        assertTrue(logger.log(new Network(FAKE_NET_ID), FAKE_TRANSPORT_TYPES, FAKE_EV));
+        assertTrue(logger.log(FAKE_INTERFACE_NAME, FAKE_EV));
+        assertTrue(logger.log(makeExpectedEvent(FAKE_TIME_STAMP, FAKE_NET_ID, TRANSPORT_WIFI,
+                FAKE_INTERFACE_NAME)));
+
+        List<ConnectivityMetricsEvent> got = verifyEvents(6);
+        assertEventsEqual(makeExpectedEvent(got.get(0).timestamp, 0, 0, null), got.get(0));
+        assertEventsEqual(makeExpectedEvent(FAKE_TIME_STAMP, 0, 0, null), got.get(1));
+        assertEventsEqual(makeExpectedEvent(got.get(2).timestamp, FAKE_NET_ID,
+                TRANSPORT_WIFI, null), got.get(2));
+        assertEventsEqual(makeExpectedEvent(got.get(3).timestamp, FAKE_NET_ID,
+                TRANSPORT_WIFI, null), got.get(3));
+        assertEventsEqual(makeExpectedEvent(got.get(4).timestamp, 0, 0, FAKE_INTERFACE_NAME),
+                got.get(4));
+        assertEventsEqual(makeExpectedEvent(FAKE_TIME_STAMP, FAKE_NET_ID,
+                TRANSPORT_WIFI, FAKE_INTERFACE_NAME), got.get(5));
+    }
+
+    @Test
+    public void testLoggingEventsWithMultipleCallers() throws Exception {
+        IpConnectivityLog logger = new IpConnectivityLog(mMockService);
+
+        final int nCallers = 10;
+        final int nEvents = 10;
+        for (int n = 0; n < nCallers; n++) {
+            final int i = n;
+            new Thread() {
+                public void run() {
+                    for (int j = 0; j < nEvents; j++) {
+                        assertTrue(logger.log(makeExpectedEvent(
+                                FAKE_TIME_STAMP + i * 100 + j,
+                                FAKE_NET_ID + i * 100 + j,
+                                ((i + j) % 2 == 0) ? TRANSPORT_WIFI : TRANSPORT_CELLULAR,
+                                FAKE_INTERFACE_NAME)));
+                    }
+                }
+            }.start();
+        }
+
+        List<ConnectivityMetricsEvent> got = verifyEvents(nCallers * nEvents, 200);
+        Collections.sort(got, EVENT_COMPARATOR);
+        Iterator<ConnectivityMetricsEvent> iter = got.iterator();
+        for (int i = 0; i < nCallers; i++) {
+            for (int j = 0; j < nEvents; j++) {
+                final long expectedTimestamp = FAKE_TIME_STAMP + i * 100 + j;
+                final int expectedNetId = FAKE_NET_ID + i * 100 + j;
+                final long expectedTransports =
+                        ((i + j) % 2 == 0) ? TRANSPORT_WIFI : TRANSPORT_CELLULAR;
+                assertEventsEqual(makeExpectedEvent(expectedTimestamp, expectedNetId,
+                        expectedTransports, FAKE_INTERFACE_NAME), iter.next());
+            }
+        }
+    }
+
+    private List<ConnectivityMetricsEvent> verifyEvents(int n, int timeoutMs) throws Exception {
+        ArgumentCaptor<ConnectivityMetricsEvent> captor =
+                ArgumentCaptor.forClass(ConnectivityMetricsEvent.class);
+        verify(mMockService, timeout(timeoutMs).times(n)).logEvent(captor.capture());
+        return captor.getAllValues();
+    }
+
+    private List<ConnectivityMetricsEvent> verifyEvents(int n) throws Exception {
+        return verifyEvents(n, 10);
+    }
+
+
+    private ConnectivityMetricsEvent makeExpectedEvent(long timestamp, int netId, long transports,
+            String ifname) {
+        ConnectivityMetricsEvent ev = new ConnectivityMetricsEvent();
+        ev.timestamp = timestamp;
+        ev.data = FAKE_EV;
+        ev.netId = netId;
+        ev.transports = transports;
+        ev.ifname = ifname;
+        return ev;
+    }
+
+    /** Outer equality for ConnectivityMetricsEvent to avoid overriding equals() and hashCode(). */
+    private void assertEventsEqual(ConnectivityMetricsEvent expected,
+            ConnectivityMetricsEvent got) {
+        assertEquals(expected.data, got.data);
+        assertEquals(expected.timestamp, got.timestamp);
+        assertEquals(expected.netId, got.netId);
+        assertEquals(expected.transports, got.transports);
+        assertEquals(expected.ifname, got.ifname);
+    }
+
+    static final Comparator<ConnectivityMetricsEvent> EVENT_COMPARATOR =
+            Comparator.comparingLong((ev) -> ev.timestamp);
+}
diff --git a/tests/common/java/android/net/metrics/IpManagerEventTest.kt b/tests/common/java/android/net/metrics/IpManagerEventTest.kt
new file mode 100644
index 0000000..64be508
--- /dev/null
+++ b/tests/common/java/android/net/metrics/IpManagerEventTest.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.metrics
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.assertParcelSane
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class IpManagerEventTest {
+    @Test
+    fun testConstructorAndParcel() {
+        (IpManagerEvent.PROVISIONING_OK..IpManagerEvent.ERROR_INTERFACE_NOT_FOUND).forEach {
+            val ipManagerEvent = IpManagerEvent(it, Long.MAX_VALUE)
+            assertEquals(it, ipManagerEvent.eventType)
+            assertEquals(Long.MAX_VALUE, ipManagerEvent.durationMs)
+
+            assertParcelSane(ipManagerEvent, 2)
+        }
+    }
+}
diff --git a/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt b/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt
new file mode 100644
index 0000000..55b5e49
--- /dev/null
+++ b/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.metrics
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.assertParcelSane
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class IpReachabilityEventTest {
+    @Test
+    fun testConstructorAndParcel() {
+        (IpReachabilityEvent.PROBE..IpReachabilityEvent.PROVISIONING_LOST_ORGANIC).forEach {
+            val ipReachabilityEvent = IpReachabilityEvent(it)
+            assertEquals(it, ipReachabilityEvent.eventType)
+
+            assertParcelSane(ipReachabilityEvent, 1)
+        }
+    }
+}
diff --git a/tests/common/java/android/net/metrics/NetworkEventTest.kt b/tests/common/java/android/net/metrics/NetworkEventTest.kt
new file mode 100644
index 0000000..41430b0
--- /dev/null
+++ b/tests/common/java/android/net/metrics/NetworkEventTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.metrics
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.assertParcelSane
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NetworkEventTest {
+    @Test
+    fun testConstructorAndParcel() {
+        (NetworkEvent.NETWORK_CONNECTED..NetworkEvent.NETWORK_PARTIAL_CONNECTIVITY).forEach {
+            var networkEvent = NetworkEvent(it)
+            assertEquals(it, networkEvent.eventType)
+            assertEquals(0, networkEvent.durationMs)
+
+            networkEvent = NetworkEvent(it, Long.MAX_VALUE)
+            assertEquals(it, networkEvent.eventType)
+            assertEquals(Long.MAX_VALUE, networkEvent.durationMs)
+
+            assertParcelSane(networkEvent, 2)
+        }
+    }
+}
diff --git a/tests/common/java/android/net/metrics/RaEventTest.kt b/tests/common/java/android/net/metrics/RaEventTest.kt
new file mode 100644
index 0000000..d9b7203
--- /dev/null
+++ b/tests/common/java/android/net/metrics/RaEventTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.metrics
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.assertParcelSane
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val NO_LIFETIME: Long = -1L
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class RaEventTest {
+    @Test
+    fun testConstructorAndParcel() {
+        var raEvent = RaEvent.Builder().build()
+        assertEquals(NO_LIFETIME, raEvent.routerLifetime)
+        assertEquals(NO_LIFETIME, raEvent.prefixValidLifetime)
+        assertEquals(NO_LIFETIME, raEvent.prefixPreferredLifetime)
+        assertEquals(NO_LIFETIME, raEvent.routeInfoLifetime)
+        assertEquals(NO_LIFETIME, raEvent.rdnssLifetime)
+        assertEquals(NO_LIFETIME, raEvent.dnsslLifetime)
+
+        raEvent = RaEvent.Builder()
+                .updateRouterLifetime(1)
+                .updatePrefixValidLifetime(2)
+                .updatePrefixPreferredLifetime(3)
+                .updateRouteInfoLifetime(4)
+                .updateRdnssLifetime(5)
+                .updateDnsslLifetime(6)
+                .build()
+        assertEquals(1, raEvent.routerLifetime)
+        assertEquals(2, raEvent.prefixValidLifetime)
+        assertEquals(3, raEvent.prefixPreferredLifetime)
+        assertEquals(4, raEvent.routeInfoLifetime)
+        assertEquals(5, raEvent.rdnssLifetime)
+        assertEquals(6, raEvent.dnsslLifetime)
+
+        raEvent = RaEvent.Builder()
+                .updateRouterLifetime(Long.MIN_VALUE)
+                .updateRouterLifetime(Long.MAX_VALUE)
+                .build()
+        assertEquals(Long.MIN_VALUE, raEvent.routerLifetime)
+
+        raEvent = RaEvent(1, 2, 3, 4, 5, 6)
+        assertEquals(1, raEvent.routerLifetime)
+        assertEquals(2, raEvent.prefixValidLifetime)
+        assertEquals(3, raEvent.prefixPreferredLifetime)
+        assertEquals(4, raEvent.routeInfoLifetime)
+        assertEquals(5, raEvent.rdnssLifetime)
+        assertEquals(6, raEvent.dnsslLifetime)
+
+        assertParcelSane(raEvent, 6)
+    }
+}
diff --git a/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt b/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt
new file mode 100644
index 0000000..51c0d41
--- /dev/null
+++ b/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.metrics
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.assertParcelSane
+import java.lang.reflect.Modifier
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val FIRST_VALIDATION: Int = 1 shl 8
+private const val REVALIDATION: Int = 2 shl 8
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ValidationProbeEventTest {
+    private infix fun Int.hasType(type: Int) = (type and this) == type
+
+    @Test
+    fun testBuilderAndParcel() {
+        var validationProbeEvent = ValidationProbeEvent.Builder()
+                .setProbeType(ValidationProbeEvent.PROBE_DNS, false).build()
+
+        assertTrue(validationProbeEvent.probeType hasType REVALIDATION)
+
+        validationProbeEvent = ValidationProbeEvent.Builder()
+                .setDurationMs(Long.MAX_VALUE)
+                .setProbeType(ValidationProbeEvent.PROBE_DNS, true)
+                .setReturnCode(ValidationProbeEvent.DNS_SUCCESS)
+                .build()
+
+        assertEquals(Long.MAX_VALUE, validationProbeEvent.durationMs)
+        assertTrue(validationProbeEvent.probeType hasType ValidationProbeEvent.PROBE_DNS)
+        assertTrue(validationProbeEvent.probeType hasType FIRST_VALIDATION)
+        assertEquals(ValidationProbeEvent.DNS_SUCCESS, validationProbeEvent.returnCode)
+
+        assertParcelSane(validationProbeEvent, 3)
+    }
+
+    @Test
+    fun testGetProbeName() {
+        val probeFields = ValidationProbeEvent::class.java.declaredFields.filter {
+            it.type == Int::class.javaPrimitiveType
+              && Modifier.isPublic(it.modifiers) && Modifier.isStatic(it.modifiers)
+              && it.name.contains("PROBE")
+        }
+
+        probeFields.forEach {
+            val intValue = it.getInt(null)
+            val stringValue = ValidationProbeEvent.getProbeName(intValue)
+            assertEquals(it.name, stringValue)
+        }
+
+    }
+}
diff --git a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
new file mode 100644
index 0000000..7b22e45
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
@@ -0,0 +1,209 @@
+/*
+ * 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
+
+import android.net.NetworkStats
+import android.net.NetworkStats.DEFAULT_NETWORK_NO
+import android.net.NetworkStats.DEFAULT_NETWORK_YES
+import android.net.NetworkStats.Entry
+import android.net.NetworkStats.IFACE_VT
+import android.net.NetworkStats.METERED_NO
+import android.net.NetworkStats.METERED_YES
+import android.net.NetworkStats.ROAMING_NO
+import android.net.NetworkStats.ROAMING_YES
+import android.net.NetworkStats.SET_DEFAULT
+import android.net.NetworkStats.SET_FOREGROUND
+import android.net.NetworkStats.TAG_NONE
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.assertFieldCountEquals
+import com.android.testutils.assertNetworkStatsEquals
+import com.android.testutils.assertParcelingIsLossless
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+
+@RunWith(JUnit4::class)
+@SmallTest
+class NetworkStatsApiTest {
+    @Rule
+    @JvmField
+    val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
+
+    private val testStatsEmpty = NetworkStats(0L, 0)
+
+    // Note that these variables need to be initialized outside of constructor, initialize
+    // here with methods that don't exist in Q devices will result in crash.
+
+    // stats1 and stats2 will have some entries with common keys, which are expected to
+    // be merged if performing add on these 2 stats.
+    private lateinit var testStats1: NetworkStats
+    private lateinit var testStats2: NetworkStats
+
+    // This is a result of adding stats1 and stats2, while the merging of common key items is
+    // subject to test later, this should not be initialized with for a loop to add stats1
+    // and stats2 above.
+    private lateinit var testStats3: NetworkStats
+
+    companion object {
+        private const val TEST_IFACE = "test0"
+        private const val TEST_UID1 = 1001
+        private const val TEST_UID2 = 1002
+    }
+
+    @Before
+    fun setUp() {
+        testStats1 = NetworkStats(0L, 0)
+                // Entries which only appear in set1.
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, 20, 3, 57, 40, 3))
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_YES, DEFAULT_NETWORK_NO, 31, 7, 24, 5, 8))
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, 25, 3, 47, 8, 2))
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 37, 52, 1, 10, 4))
+                // Entries which are common for set1 and set2.
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 101, 2, 103, 4, 5))
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 17, 2, 11, 1, 0))
+                .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 40, 1, 0, 0, 8))
+                .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 1, 6, 2, 0))
+        assertEquals(8, testStats1.size())
+
+        testStats2 = NetworkStats(0L, 0)
+                // Entries which are common for set1 and set2.
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1))
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45))
+                .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7))
+                .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0))
+                // Entry which only appears in set2.
+                .addEntry(Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
+        assertEquals(5, testStats2.size())
+
+        testStats3 = NetworkStats(0L, 9)
+                // Entries which are unique either in stats1 or stats2.
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 101, 2, 103, 4, 5))
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_YES, DEFAULT_NETWORK_NO, 31, 7, 24, 5, 8))
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, 25, 3, 47, 8, 2))
+                .addEntry(Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
+                // Entries which are common for stats1 and stats2 are being merged.
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, 20, 3, 57, 40, 3))
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 20, 17, 13, 32, 1))
+                .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 50, 113, 11, 11, 49))
+                .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 51, 3, 3, 4, 15))
+                .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 7, 4, 8, 3, 0))
+        assertEquals(9, testStats3.size())
+    }
+
+    @Test
+    fun testAddEntry() {
+        val expectedEntriesInStats2 = arrayOf(
+                Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1),
+                Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45),
+                Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7),
+                Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0),
+                Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE,
+                        METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0))
+
+        // While testStats* are already initialized with addEntry, verify content added
+        // matches expectation.
+        for (i in expectedEntriesInStats2.indices) {
+            val entry = testStats2.getValues(i, null)
+            assertEquals(expectedEntriesInStats2[i], entry)
+        }
+
+        // Verify entry updated with addEntry.
+        val stats = testStats2.addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12, -5, 7, 0, 9))
+        assertEquals(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE,
+                METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16, -2, 9, 1, 9),
+                stats.getValues(3, null))
+    }
+
+    @Test
+    fun testAdd() {
+        var stats = NetworkStats(0L, 0)
+        assertNetworkStatsEquals(testStatsEmpty, stats)
+        stats = stats.add(testStats2)
+        assertNetworkStatsEquals(testStats2, stats)
+        stats = stats.add(testStats1)
+        // EMPTY + STATS2 + STATS1 = STATS3
+        assertNetworkStatsEquals(testStats3, stats)
+    }
+
+    @Test
+    fun testParcelUnparcel() {
+        assertParcelingIsLossless(testStatsEmpty)
+        assertParcelingIsLossless(testStats1)
+        assertParcelingIsLossless(testStats2)
+        assertFieldCountEquals(15, NetworkStats::class.java)
+    }
+
+    @Test
+    fun testDescribeContents() {
+        assertEquals(0, testStatsEmpty.describeContents())
+        assertEquals(0, testStats1.describeContents())
+        assertEquals(0, testStats2.describeContents())
+        assertEquals(0, testStats3.describeContents())
+    }
+
+    @Test
+    fun testSubtract() {
+        // STATS3 - STATS2 = STATS1
+        assertNetworkStatsEquals(testStats1, testStats3.subtract(testStats2))
+        // STATS3 - STATS1 = STATS2
+        assertNetworkStatsEquals(testStats2, testStats3.subtract(testStats1))
+    }
+
+    @Test
+    fun testMethodsDontModifyReceiver() {
+        listOf(testStatsEmpty, testStats1, testStats2, testStats3).forEach {
+            val origStats = it.clone()
+            it.addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE,
+                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45))
+            it.add(testStats3)
+            it.subtract(testStats1)
+            assertNetworkStatsEquals(origStats, it)
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/util/SocketUtilsTest.kt b/tests/common/java/android/net/util/SocketUtilsTest.kt
new file mode 100644
index 0000000..aaf97f3
--- /dev/null
+++ b/tests/common/java/android/net/util/SocketUtilsTest.kt
@@ -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.util
+
+import android.os.Build
+import android.system.NetlinkSocketAddress
+import android.system.Os
+import android.system.OsConstants.AF_INET
+import android.system.OsConstants.ETH_P_ALL
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.RTMGRP_NEIGH
+import android.system.OsConstants.SOCK_DGRAM
+import android.system.PacketSocketAddress
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_INDEX = 123
+private const val TEST_PORT = 555
+private const val FF_BYTE = 0xff.toByte()
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class SocketUtilsTest {
+    @Rule @JvmField
+    val ignoreRule = DevSdkIgnoreRule()
+
+    @Test
+    fun testMakeNetlinkSocketAddress() {
+        val nlAddress = SocketUtils.makeNetlinkSocketAddress(TEST_PORT, RTMGRP_NEIGH)
+        if (nlAddress is NetlinkSocketAddress) {
+            assertEquals(TEST_PORT, nlAddress.getPortId())
+            assertEquals(RTMGRP_NEIGH, nlAddress.getGroupsMask())
+        } else {
+            fail("Not NetlinkSocketAddress object")
+        }
+    }
+
+    @Test
+    fun testMakePacketSocketAddress_Q() {
+        val pkAddress = SocketUtils.makePacketSocketAddress(ETH_P_ALL, TEST_INDEX)
+        assertTrue("Not PacketSocketAddress object", pkAddress is PacketSocketAddress)
+
+        val pkAddress2 = SocketUtils.makePacketSocketAddress(TEST_INDEX, ByteArray(6) { FF_BYTE })
+        assertTrue("Not PacketSocketAddress object", pkAddress2 is PacketSocketAddress)
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    fun testMakePacketSocketAddress() {
+        val pkAddress = SocketUtils.makePacketSocketAddress(
+                ETH_P_ALL, TEST_INDEX, ByteArray(6) { FF_BYTE })
+        assertTrue("Not PacketSocketAddress object", pkAddress is PacketSocketAddress)
+    }
+
+    @Test
+    fun testCloseSocket() {
+        // Expect no exception happening with null object.
+        SocketUtils.closeSocket(null)
+
+        val fd = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
+        assertTrue(fd.valid())
+        SocketUtils.closeSocket(fd)
+        assertFalse(fd.valid())
+        // Expecting socket should be still invalid even closed socket again.
+        SocketUtils.closeSocket(fd)
+        assertFalse(fd.valid())
+    }
+}
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 1afbfb0..defea4c 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
@@ -106,6 +106,8 @@
 
     private static final String KEY_NETWORK_STATE_OBSERVER = TEST_PKG + ".observer";
 
+    private static final String EMPTY_STRING = "";
+
     protected static final int TYPE_COMPONENT_ACTIVTIY = 0;
     protected static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1;
 
@@ -206,6 +208,8 @@
                 final String resultData = getResultData();
                 if (resultData == null) {
                     Log.e(TAG, "Received null data from ordered intent");
+                    // Offer an empty string so that the code waiting for the result can return.
+                    result.offer(EMPTY_STRING);
                     return;
                 }
                 result.offer(resultData);
@@ -893,4 +897,15 @@
          */
         String getExpected();
     }
+
+    protected void setRestrictedNetworkingMode(boolean enabled) throws Exception {
+        executeSilentShellCommand(
+                "settings put global restricted_networking_mode " + (enabled ? 1 : 0));
+        assertRestrictedNetworkingModeState(enabled);
+    }
+
+    protected void assertRestrictedNetworkingModeState(boolean enabled) throws Exception {
+        assertDelayedShellCommand("cmd netpolicy get restricted-mode",
+                "Restricted mode status: " + (enabled ? "enabled" : "disabled"));
+    }
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
new file mode 100644
index 0000000..ddc5fd4
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyManagerTest.java
@@ -0,0 +1,241 @@
+/*
+ * 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.cts.net.hostside;
+
+import static android.os.Process.SYSTEM_UID;
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.assertNetworkingBlockedStatusForUid;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isUidNetworkingBlocked;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isUidRestrictedOnMeteredNetworks;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class NetworkPolicyManagerTest extends AbstractRestrictBackgroundNetworkTestCase {
+    private static final boolean METERED = true;
+    private static final boolean NON_METERED = false;
+
+    @Rule
+    public final MeterednessConfigurationRule mMeterednessConfiguration =
+            new MeterednessConfigurationRule();
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        assumeTrue(canChangeActiveNetworkMeteredness());
+
+        registerBroadcastReceiver();
+
+        removeRestrictBackgroundWhitelist(mUid);
+        removeRestrictBackgroundBlacklist(mUid);
+        assertRestrictBackgroundChangedReceived(0);
+
+        // Initial state
+        setBatterySaverMode(false);
+        setRestrictBackground(false);
+        setRestrictedNetworkingMode(false);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+
+        setBatterySaverMode(false);
+        setRestrictBackground(false);
+        setRestrictedNetworkingMode(false);
+        unregisterNetworkCallback();
+    }
+
+    @Test
+    public void testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
+        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
+        // test the cases of non-metered network and uid not matched by any rule.
+        // If mUid is not blocked by data saver mode or power saver mode, no matter the network is
+        // metered or non-metered, mUid shouldn't be blocked.
+        assertFalse(isUidNetworkingBlocked(mUid, METERED)); // Match NTWK_ALLOWED_DEFAULT
+        assertFalse(isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
+    }
+
+    @Test
+    public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
+        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
+        // test the case of uid is system uid.
+        // SYSTEM_UID will never be blocked.
+        assertFalse(isUidNetworkingBlocked(SYSTEM_UID, METERED)); // Match NTWK_ALLOWED_SYSTEM
+        assertFalse(isUidNetworkingBlocked(SYSTEM_UID, NON_METERED)); // Match NTWK_ALLOWED_SYSTEM
+        try {
+            setRestrictBackground(true);
+            setBatterySaverMode(true);
+            setRestrictedNetworkingMode(true);
+            assertNetworkingBlockedStatusForUid(SYSTEM_UID, METERED,
+                    false /* expectedResult */); // Match NTWK_ALLOWED_SYSTEM
+            assertFalse(
+                    isUidNetworkingBlocked(SYSTEM_UID, NON_METERED)); // Match NTWK_ALLOWED_SYSTEM
+        } finally {
+            setRestrictBackground(false);
+            setBatterySaverMode(false);
+            setRestrictedNetworkingMode(false);
+            assertNetworkingBlockedStatusForUid(mUid, METERED,
+                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
+        }
+    }
+
+    @Test
+    public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
+        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
+        // test the cases of non-metered network, uid is matched by restrict background blacklist,
+        // uid is matched by restrict background whitelist, app is in the foreground with restrict
+        // background enabled and the app is in the background with restrict background enabled.
+        try {
+            // Enable restrict background and mUid will be blocked because it's not in the
+            // foreground.
+            setRestrictBackground(true);
+            assertNetworkingBlockedStatusForUid(mUid, METERED,
+                    true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
+
+            // Although restrict background is enabled and mUid is in the background, but mUid will
+            // not be blocked if network is non-metered.
+            assertFalse(
+                    isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
+
+            // Add mUid into the restrict background blacklist.
+            addRestrictBackgroundBlacklist(mUid);
+            assertNetworkingBlockedStatusForUid(mUid, METERED,
+                    true /* expectedResult */); // Match NTWK_BLOCKED_DENYLIST
+
+            // Although mUid is in the restrict background blacklist, but mUid won't be blocked if
+            // the network is non-metered.
+            assertFalse(
+                    isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
+            removeRestrictBackgroundBlacklist(mUid);
+
+            // Add mUid into the restrict background whitelist.
+            addRestrictBackgroundWhitelist(mUid);
+            assertNetworkingBlockedStatusForUid(mUid, METERED,
+                    false /* expectedResult */); // Match NTWK_ALLOWED_ALLOWLIST
+            assertFalse(
+                    isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
+            removeRestrictBackgroundWhitelist(mUid);
+
+            // Make TEST_APP2_PKG go to foreground and mUid will be allowed temporarily.
+            launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+            assertForegroundState();
+            assertNetworkingBlockedStatusForUid(mUid, METERED,
+                    false /* expectedResult */); // Match NTWK_ALLOWED_TMP_ALLOWLIST
+
+            // Back to background.
+            finishActivity();
+            assertNetworkingBlockedStatusForUid(mUid, METERED,
+                    true /* expectedResult */); // Match NTWK_BLOCKED_BG_RESTRICT
+        } finally {
+            setRestrictBackground(false);
+            assertNetworkingBlockedStatusForUid(mUid, METERED,
+                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
+        }
+    }
+
+    @Test
+    public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
+        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
+        // test the cases of restricted networking mode enabled.
+        try {
+            // All apps should be blocked if restricted networking mode is enabled except for those
+            // apps who have CONNECTIVITY_USE_RESTRICTED_NETWORKS permission.
+            // This test won't test if an app who has CONNECTIVITY_USE_RESTRICTED_NETWORKS will not
+            // be blocked because CONNECTIVITY_USE_RESTRICTED_NETWORKS is a signature/privileged
+            // permission that CTS cannot acquire. Also it's not good for this test to use those
+            // privileged apps which have CONNECTIVITY_USE_RESTRICTED_NETWORKS to test because there
+            // is no guarantee that those apps won't remove this permission someday, and if it
+            // happens, then this test will fail.
+            setRestrictedNetworkingMode(true);
+            assertNetworkingBlockedStatusForUid(mUid, METERED,
+                    true /* expectedResult */); // Match NTWK_BLOCKED_RESTRICTED_MODE
+            assertTrue(isUidNetworkingBlocked(mUid,
+                    NON_METERED)); // Match NTWK_BLOCKED_RESTRICTED_MODE
+        } finally {
+            setRestrictedNetworkingMode(false);
+            assertNetworkingBlockedStatusForUid(mUid, METERED,
+                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
+        }
+    }
+
+    @Test
+    public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
+        // Refer to NetworkPolicyManagerService#isUidNetworkingBlockedInternal(), this test is to
+        // test the cases of power saver mode enabled, uid in the power saver mode whitelist and
+        // uid in the power saver mode whitelist with non-metered network.
+        try {
+            // mUid should be blocked if power saver mode is enabled.
+            setBatterySaverMode(true);
+            assertNetworkingBlockedStatusForUid(mUid, METERED,
+                    true /* expectedResult */); // Match NTWK_BLOCKED_POWER
+            assertTrue(isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_BLOCKED_POWER
+
+            // Add TEST_APP2_PKG into power saver mode whitelist, its uid rule is RULE_ALLOW_ALL and
+            // it shouldn't be blocked.
+            addPowerSaveModeWhitelist(TEST_APP2_PKG);
+            assertNetworkingBlockedStatusForUid(mUid, METERED,
+                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
+            assertFalse(
+                    isUidNetworkingBlocked(mUid, NON_METERED)); // Match NTWK_ALLOWED_NON_METERED
+            removePowerSaveModeWhitelist(TEST_APP2_PKG);
+        } finally {
+            setBatterySaverMode(false);
+            assertNetworkingBlockedStatusForUid(mUid, METERED,
+                    false /* expectedResult */); // Match NTWK_ALLOWED_DEFAULT
+        }
+    }
+
+    @Test
+    public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
+        try {
+            // isUidRestrictedOnMeteredNetworks() will only return true when restrict background is
+            // enabled and mUid is not in the restrict background whitelist and TEST_APP2_PKG is not
+            // in the foreground. For other cases, it will return false.
+            setRestrictBackground(true);
+            assertTrue(isUidRestrictedOnMeteredNetworks(mUid));
+
+            // Make TEST_APP2_PKG go to foreground and isUidRestrictedOnMeteredNetworks() will
+            // return false.
+            launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+            assertForegroundState();
+            assertFalse(isUidRestrictedOnMeteredNetworks(mUid));
+            // Back to background.
+            finishActivity();
+
+            // Add mUid into restrict background whitelist and isUidRestrictedOnMeteredNetworks()
+            // will return false.
+            addRestrictBackgroundWhitelist(mUid);
+            assertFalse(isUidRestrictedOnMeteredNetworks(mUid));
+            removeRestrictBackgroundWhitelist(mUid);
+        } finally {
+            // Restrict background is disabled and isUidRestrictedOnMeteredNetworks() will return
+            // false.
+            setRestrictBackground(false);
+            assertFalse(isUidRestrictedOnMeteredNetworks(mUid));
+        }
+    }
+}
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 e62d557..05576cc 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
@@ -43,6 +43,7 @@
 import android.net.ConnectivityManager.NetworkCallback;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.NetworkPolicyManager;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiManager.ActionListener;
@@ -57,6 +58,7 @@
 
 import com.android.compatibility.common.util.AppStandbyUtils;
 import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.PollingCheck;
 import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.compatibility.common.util.ThrowingRunnable;
 
@@ -80,6 +82,7 @@
     private static ConnectivityManager mCm;
     private static WifiManager mWm;
     private static CarrierConfigManager mCarrierConfigManager;
+    private static NetworkPolicyManager sNpm;
 
     private static Boolean mBatterySaverSupported;
     private static Boolean mDataSaverSupported;
@@ -401,6 +404,13 @@
         return mCarrierConfigManager;
     }
 
+    public static NetworkPolicyManager getNetworkPolicyManager() {
+        if (sNpm == null) {
+            sNpm = getContext().getSystemService(NetworkPolicyManager.class);
+        }
+        return sNpm;
+    }
+
     public static Context getContext() {
         return getInstrumentation().getContext();
     }
@@ -408,4 +418,33 @@
     public static Instrumentation getInstrumentation() {
         return InstrumentationRegistry.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
+    // flaky rate.
+    public static void assertNetworkingBlockedStatusForUid(int uid, boolean metered,
+            boolean expectedResult) throws Exception {
+        PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)));
+    }
+
+    public static boolean isUidNetworkingBlocked(int uid, boolean meteredNetwork) {
+        final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            return getNetworkPolicyManager().isUidNetworkingBlocked(uid, meteredNetwork);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    public static boolean isUidRestrictedOnMeteredNetworks(int uid) {
+        final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
+        try {
+            uiAutomation.adoptShellPermissionIdentity();
+            return getNetworkPolicyManager().isUidRestrictedOnMeteredNetworks(uid);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
 }
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
index 29d3c6e..5f0f6d6 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
@@ -28,28 +28,17 @@
 
     @After
     public void tearDown() throws Exception {
-        setRestrictedMode(false);
+        setRestrictedNetworkingMode(false);
         super.tearDown();
     }
 
-    private void setRestrictedMode(boolean enabled) throws Exception {
-        executeSilentShellCommand(
-                "settings put global restricted_networking_mode " + (enabled ? 1 : 0));
-        assertRestrictedModeState(enabled);
-    }
-
-    private void assertRestrictedModeState(boolean enabled) throws Exception {
-        assertDelayedShellCommand("cmd netpolicy get restricted-mode",
-                "Restricted mode status: " + (enabled ? "enabled" : "disabled"));
-    }
-
     @Test
     public void testNetworkAccess() throws Exception {
-        setRestrictedMode(false);
+        setRestrictedNetworkingMode(false);
 
         // go to foreground state and enable restricted mode
         launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
-        setRestrictedMode(true);
+        setRestrictedNetworkingMode(true);
         assertForegroundNetworkAccess(false);
 
         // go to background state
@@ -57,7 +46,7 @@
         assertBackgroundNetworkAccess(false);
 
         // disable restricted mode and assert network access in foreground and background states
-        setRestrictedMode(false);
+        setRestrictedNetworkingMode(false);
         launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
         assertForegroundNetworkAccess(true);
 
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 532fd86..62aa493 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -17,6 +17,7 @@
 package com.android.cts.net.hostside;
 
 import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.ConnectivityManager.TYPE_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.os.Process.INVALID_UID;
 import static android.system.OsConstants.AF_INET;
@@ -759,6 +760,7 @@
         assertEquals(vpnNetwork, mCM.getActiveNetwork());
         assertNotEqual(defaultNetwork, vpnNetwork);
         maybeExpectVpnTransportInfo(vpnNetwork);
+        assertEquals(TYPE_VPN, mCM.getNetworkInfo(vpnNetwork).getType());
 
         if (SdkLevel.isAtLeastS()) {
             // Check that system default network callback has not seen any network changes, even
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
new file mode 100644
index 0000000..fdb8876
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkPolicyManagerTests.java
@@ -0,0 +1,66 @@
+/*
+ * 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.cts.net;
+
+public class HostsideNetworkPolicyManagerTests extends HostsideNetworkTestCase {
+    @Override
+    protected 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 testIsUidNetworkingBlocked_withUidNotBlocked() throws Exception {
+        runDeviceTests(TEST_PKG,
+                TEST_PKG + ".NetworkPolicyManagerTest",
+                "testIsUidNetworkingBlocked_withUidNotBlocked");
+    }
+
+    public void testIsUidNetworkingBlocked_withSystemUid() throws Exception {
+        runDeviceTests(TEST_PKG,
+                TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidNetworkingBlocked_withSystemUid");
+    }
+
+    public void testIsUidNetworkingBlocked_withDataSaverMode() throws Exception {
+        runDeviceTests(TEST_PKG,
+                TEST_PKG + ".NetworkPolicyManagerTest",
+                "testIsUidNetworkingBlocked_withDataSaverMode");
+    }
+
+    public void testIsUidNetworkingBlocked_withRestrictedNetworkingMode() throws Exception {
+        runDeviceTests(TEST_PKG,
+                TEST_PKG + ".NetworkPolicyManagerTest",
+                "testIsUidNetworkingBlocked_withRestrictedNetworkingMode");
+    }
+
+    public void testIsUidNetworkingBlocked_withPowerSaverMode() throws Exception {
+        runDeviceTests(TEST_PKG,
+                TEST_PKG + ".NetworkPolicyManagerTest",
+                "testIsUidNetworkingBlocked_withPowerSaverMode");
+    }
+
+    public void testIsUidRestrictedOnMeteredNetworks() throws Exception {
+        runDeviceTests(TEST_PKG,
+                TEST_PKG + ".NetworkPolicyManagerTest", "testIsUidRestrictedOnMeteredNetworks");
+    }
+}
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index cd69b13..e28c33f 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -38,6 +38,7 @@
     srcs: [
         "src/**/*.java",
         "src/**/*.kt",
+        ":ike-aes-xcbc",
     ],
     jarjar_rules: "jarjar-rules-shared.txt",
     static_libs: [
diff --git a/tests/cts/net/jni/Android.bp b/tests/cts/net/jni/Android.bp
index 13f38d7..8f0d78f 100644
--- a/tests/cts/net/jni/Android.bp
+++ b/tests/cts/net/jni/Android.bp
@@ -16,35 +16,14 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-cc_library_shared {
-    name: "libnativedns_jni",
+cc_defaults {
+    name: "net_jni_defaults",
 
-    srcs: ["NativeDnsJni.c"],
-    sdk_version: "current",
-
-    shared_libs: [
-        "libnativehelper_compat_libc++",
-        "liblog",
-    ],
-    stl: "libc++_static",
-
-    cflags: [
-        "-Wall",
-        "-Werror",
-        "-Wno-unused-parameter",
-    ],
-
-}
-
-cc_library_shared {
-    name: "libnativemultinetwork_jni",
-
-    srcs: ["NativeMultinetworkJni.cpp"],
-    sdk_version: "current",
     cflags: [
         "-Wall",
         "-Werror",
         "-Wno-format",
+        "-Wno-unused-parameter",
     ],
     shared_libs: [
         "libandroid",
@@ -52,4 +31,19 @@
         "liblog",
     ],
     stl: "libc++_static",
+    // To be compatible with Q devices, the min_sdk_version must be 29.
+    sdk_version: "current",
+    min_sdk_version: "29",
+}
+
+cc_library_shared {
+    name: "libnativedns_jni",
+    defaults: ["net_jni_defaults"],
+    srcs: ["NativeDnsJni.c"],
+}
+
+cc_library_shared {
+    name: "libnativemultinetwork_jni",
+    defaults: ["net_jni_defaults"],
+    srcs: ["NativeMultinetworkJni.cpp"],
 }
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index 5e9af8e..434e529 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -24,6 +24,8 @@
         "liblog",
         "libutils",
     ],
+    // To be compatible with Q devices, the min_sdk_version must be 29.
+    min_sdk_version: "29",
 }
 
 cc_test {
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index a889c41..9f079c4 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -20,6 +20,7 @@
 import android.Manifest.permission.NETWORK_SETTINGS
 import android.Manifest.permission.READ_DEVICE_CONFIG
 import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WATCH
 import android.content.pm.PackageManager.FEATURE_WIFI
 import android.net.ConnectivityManager
 import android.net.ConnectivityManager.NetworkCallback
@@ -57,6 +58,7 @@
 import junit.framework.AssertionFailedError
 import org.junit.After
 import org.junit.Assume.assumeTrue
+import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.runner.RunWith
 import java.util.concurrent.CompletableFuture
@@ -128,6 +130,7 @@
     fun testCaptivePortalIsNotDefaultNetwork() {
         assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
         assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
+        assumeFalse(pm.hasSystemFeature(FEATURE_WATCH))
         utils.ensureWifiConnected()
         val cellNetwork = utils.connectToCell()
 
@@ -148,8 +151,8 @@
         server.addResponse(Request(TEST_PORTAL_URL_PATH), Status.OK,
                 content = "Test captive portal content")
         server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR)
-        server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT,
-                locationHeader = makeUrl(TEST_PORTAL_URL_PATH))
+        val headers = mapOf("Location" to makeUrl(TEST_PORTAL_URL_PATH))
+        server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT, headers)
         setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH))
         setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH))
         // URL expiration needs to be in the next 10 minutes
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityFrameworkInitializerTest.kt b/tests/cts/net/src/android/net/cts/ConnectivityFrameworkInitializerTest.kt
new file mode 100644
index 0000000..d687eaa
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ConnectivityFrameworkInitializerTest.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.net.ConnectivityDiagnosticsManager
+import android.net.ConnectivityFrameworkInitializer
+import android.net.ConnectivityManager
+import android.net.TestNetworkManager
+import android.os.Build
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.runAsShell
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertNotNull
+
+@RunWith(DevSdkIgnoreRunner::class)
+// ConnectivityFrameworkInitializer was added in S
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class ConnectivityFrameworkInitializerTest {
+    @Test
+    fun testServicesRegistered() {
+        val ctx = InstrumentationRegistry.getInstrumentation().context as android.content.Context
+        assertNotNull(ctx.getSystemService(ConnectivityManager::class.java),
+                "ConnectivityManager not registered")
+        assertNotNull(ctx.getSystemService(ConnectivityDiagnosticsManager::class.java),
+                "ConnectivityDiagnosticsManager not registered")
+
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            assertNotNull(ctx.getSystemService(TestNetworkManager::class.java),
+                "TestNetworkManager not registered")
+        }
+        // Do not test for DnsResolverServiceManager as it is internal to Connectivity, and
+        // CTS does not have permission to get it anyway.
+    }
+
+    // registerServiceWrappers can only be called during initialization and should throw otherwise
+    @Test(expected = IllegalStateException::class)
+    fun testThrows() {
+        ConnectivityFrameworkInitializer.registerServiceWrappers()
+    }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index fda128c..8b57a92 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -19,6 +19,7 @@
 import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
 import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
 import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.content.pm.PackageManager.FEATURE_BLUETOOTH;
 import static android.content.pm.PackageManager.FEATURE_ETHERNET;
 import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
@@ -28,6 +29,7 @@
 import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
 import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
 import static android.net.ConnectivityManager.TYPE_ETHERNET;
 import static android.net.ConnectivityManager.TYPE_MOBILE_CBS;
@@ -47,28 +49,40 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TetheringRequest;
 import static android.net.cts.util.CtsNetUtils.ConnectivityActionReceiver;
 import static android.net.cts.util.CtsNetUtils.HTTP_PORT;
 import static android.net.cts.util.CtsNetUtils.NETWORK_CALLBACK_ACTION;
 import static android.net.cts.util.CtsNetUtils.TEST_HOST;
 import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+import static android.net.cts.util.CtsTetheringUtils.StartTetheringCallback;
+import static android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
+import static android.net.cts.util.CtsTetheringUtils.isWifiTetheringSupported;
+import static android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL;
+import static android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL;
 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
 import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
 import static android.system.OsConstants.AF_INET;
 import static android.system.OsConstants.AF_INET6;
 import static android.system.OsConstants.AF_UNSPEC;
 
+import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
 import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
 import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
 import static com.android.testutils.MiscAsserts.assertThrows;
+import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
@@ -90,6 +104,7 @@
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivitySettingsManager;
 import android.net.InetAddresses;
 import android.net.IpSecManager;
 import android.net.IpSecManager.UdpEncapsulationSocket;
@@ -101,10 +116,17 @@
 import android.net.NetworkInfo.DetailedState;
 import android.net.NetworkInfo.State;
 import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.net.NetworkStateSnapshot;
 import android.net.NetworkUtils;
+import android.net.OemNetworkPreferences;
+import android.net.ProxyInfo;
 import android.net.SocketKeepalive;
+import android.net.TelephonyNetworkSpecifier;
 import android.net.TestNetworkInterface;
 import android.net.TestNetworkManager;
+import android.net.TetheringManager;
+import android.net.Uri;
 import android.net.cts.util.CtsNetUtils;
 import android.net.util.KeepaliveUtils;
 import android.net.wifi.WifiManager;
@@ -119,7 +141,9 @@
 import android.os.UserHandle;
 import android.os.VintfRuntimeInfo;
 import android.platform.test.annotations.AppModeFull;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.ArraySet;
@@ -137,9 +161,12 @@
 import com.android.networkstack.apishim.common.ConnectivityManagerShim;
 import com.android.testutils.CompatUtil;
 import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.DevSdkIgnoreRuleKt;
 import com.android.testutils.RecorderCallback.CallbackEntry;
 import com.android.testutils.SkipPresubmit;
+import com.android.testutils.TestHttpServer;
+import com.android.testutils.TestNetworkTracker;
 import com.android.testutils.TestableNetworkCallback;
 
 import junit.framework.AssertionFailedError;
@@ -170,15 +197,22 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import fi.iki.elonen.NanoHTTPD.Method;
+import fi.iki.elonen.NanoHTTPD.Response.IStatus;
+import fi.iki.elonen.NanoHTTPD.Response.Status;
+
 @RunWith(AndroidJUnit4.class)
 public class ConnectivityManagerTest {
     @Rule
@@ -219,6 +253,15 @@
     private static final LinkAddress TEST_LINKADDR = new LinkAddress(
             InetAddresses.parseNumericAddress("2001:db8::8"), 64);
 
+    private static final int AIRPLANE_MODE_OFF = 0;
+    private static final int AIRPLANE_MODE_ON = 1;
+
+    private static final String TEST_HTTPS_URL_PATH = "/https_path";
+    private static final String TEST_HTTP_URL_PATH = "/http_path";
+    private static final String LOCALHOST_HOSTNAME = "localhost";
+    // Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
+    private static final long WIFI_CONNECT_TIMEOUT_MS = 60_000L;
+
     private Context mContext;
     private Instrumentation mInstrumentation;
     private ConnectivityManager mCm;
@@ -228,10 +271,13 @@
     private final ArraySet<Integer> mNetworkTypes = new ArraySet<>();
     private UiAutomation mUiAutomation;
     private CtsNetUtils mCtsNetUtils;
+    private TetheringManager mTm;
 
     // Used for cleanup purposes.
     private final List<Range<Integer>> mVpnRequiredUidRanges = new ArrayList<>();
 
+    private final TestHttpServer mHttpServer = new TestHttpServer(LOCALHOST_HOSTNAME);
+
     @Before
     public void setUp() throws Exception {
         mInstrumentation = InstrumentationRegistry.getInstrumentation();
@@ -241,6 +287,7 @@
         mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
         mPackageManager = mContext.getPackageManager();
         mCtsNetUtils = new CtsNetUtils(mContext);
+        mTm = mContext.getSystemService(TetheringManager.class);
 
         if (DevSdkIgnoreRuleKt.isDevSdkInRange(null /* minExclusive */,
                 Build.VERSION_CODES.R /* maxInclusive */)) {
@@ -426,6 +473,69 @@
         }
     }
 
+    private String getSubscriberIdForCellNetwork(Network cellNetwork) {
+        final NetworkCapabilities cellCaps = mCm.getNetworkCapabilities(cellNetwork);
+        final NetworkSpecifier specifier = cellCaps.getNetworkSpecifier();
+        assertTrue(specifier instanceof TelephonyNetworkSpecifier);
+        // Get subscription from Telephony network specifier.
+        final int subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
+        assertNotEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID, subId);
+
+        // Get subscriber Id from telephony manager.
+        final TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+        return runWithShellPermissionIdentity(() -> tm.getSubscriberId(subId),
+                android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
+    }
+
+    @AppModeFull(reason = "Cannot request network in instant app mode")
+    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testGetAllNetworkStateSnapshots()
+            throws InterruptedException {
+        // Make sure cell is active to retrieve IMSI for verification in later step.
+        final Network cellNetwork = mCtsNetUtils.connectToCell();
+        final String subscriberId = getSubscriberIdForCellNetwork(cellNetwork);
+        assertFalse(TextUtils.isEmpty(subscriberId));
+
+        // Verify the API cannot be called without proper permission.
+        assertThrows(SecurityException.class, () -> mCm.getAllNetworkStateSnapshots());
+
+        // Get all networks, verify the result of getAllNetworkStateSnapshots matches the result
+        // got from other APIs.
+        final Network[] networks = mCm.getAllNetworks();
+        assertGreaterOrEqual(networks.length, 1);
+        final List<NetworkStateSnapshot> snapshots = runWithShellPermissionIdentity(
+                () -> mCm.getAllNetworkStateSnapshots(), NETWORK_SETTINGS);
+        assertEquals(networks.length, snapshots.size());
+        for (final Network network : networks) {
+            // Can't use a lambda because it will cause the test to crash on R with
+            // NoClassDefFoundError.
+            NetworkStateSnapshot snapshot = null;
+            for (NetworkStateSnapshot item : snapshots) {
+                if (item.getNetwork().equals(network)) {
+                    snapshot = item;
+                    break;
+                }
+            }
+            assertNotNull(snapshot);
+            final NetworkCapabilities caps =
+                    Objects.requireNonNull(mCm.getNetworkCapabilities(network));
+            // Redact specifier of the capabilities of the snapshot before comparing since
+            // the result returned from getNetworkCapabilities always get redacted.
+            final NetworkSpecifier redactedSnapshotCapSpecifier =
+                    snapshot.getNetworkCapabilities().getNetworkSpecifier().redact();
+            assertEquals("", caps.describeImmutableDifferences(
+                    snapshot.getNetworkCapabilities()
+                            .setNetworkSpecifier(redactedSnapshotCapSpecifier)));
+            assertEquals(mCm.getLinkProperties(network), snapshot.getLinkProperties());
+            assertEquals(mCm.getNetworkInfo(network).getType(), snapshot.getLegacyType());
+
+            if (network.equals(cellNetwork)) {
+                assertEquals(subscriberId, snapshot.getSubscriberId());
+            }
+        }
+    }
+
     /**
      * Tests that connections can be opened on WiFi and cellphone networks,
      * and that they are made from different IP addresses.
@@ -434,13 +544,8 @@
     @Test
     @SkipPresubmit(reason = "Virtual devices use a single internet connection for all networks")
     public void testOpenConnection() throws Exception {
-        boolean canRunTest = mPackageManager.hasSystemFeature(FEATURE_WIFI)
-                && mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
-        if (!canRunTest) {
-            Log.i(TAG,"testOpenConnection cannot execute unless device supports both WiFi "
-                    + "and a cellular connection");
-            return;
-        }
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
 
         Network wifiNetwork = mCtsNetUtils.connectToWifi();
         Network cellNetwork = mCtsNetUtils.connectToCell();
@@ -566,18 +671,57 @@
         mCm.getBackgroundDataSetting();
     }
 
+    private NetworkRequest makeDefaultRequest() {
+        // Make a request that is similar to the way framework tracks the system
+        // default network.
+        return new NetworkRequest.Builder()
+                .clearCapabilities()
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .build();
+    }
+
     private NetworkRequest makeWifiNetworkRequest() {
         return new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET)
                 .build();
     }
 
     private NetworkRequest makeCellNetworkRequest() {
         return new NetworkRequest.Builder()
                 .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
                 .build();
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    public void testIsPrivateDnsBroken() throws InterruptedException {
+        final String invalidPrivateDnsServer = "invalidhostname.example.com";
+        final String goodPrivateDnsServer = "dns.google";
+        mCtsNetUtils.storePrivateDnsSetting();
+        final TestableNetworkCallback cb = new TestableNetworkCallback();
+        mCm.registerNetworkCallback(makeWifiNetworkRequest(), cb);
+        try {
+            // Verifying the good private DNS sever
+            mCtsNetUtils.setPrivateDnsStrictMode(goodPrivateDnsServer);
+            final Network networkForPrivateDns =  mCtsNetUtils.ensureWifiConnected();
+            cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
+                    entry -> (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+                    .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
+
+            // Verifying the broken private DNS sever
+            mCtsNetUtils.setPrivateDnsStrictMode(invalidPrivateDnsServer);
+            cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, NETWORK_CALLBACK_TIMEOUT_MS,
+                    entry -> (((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+                    .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
+        } finally {
+            mCtsNetUtils.restorePrivateDnsSetting();
+        }
+    }
+
     /**
      * Exercises both registerNetworkCallback and unregisterNetworkCallback. This checks to
      * see if we get a callback for the TRANSPORT_WIFI transport type being available.
@@ -590,10 +734,7 @@
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
     public void testRegisterNetworkCallback() {
-        if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
-            Log.i(TAG, "testRegisterNetworkCallback cannot execute unless device supports WiFi");
-            return;
-        }
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
         // We will register for a WIFI network being available or lost.
         final TestNetworkCallback callback = new TestNetworkCallback();
@@ -604,12 +745,14 @@
 
         final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback();
         final TestNetworkCallback perUidCallback = new TestNetworkCallback();
+        final TestNetworkCallback bestMatchingCallback = new TestNetworkCallback();
         final Handler h = new Handler(Looper.getMainLooper());
         if (TestUtils.shouldTestSApis()) {
             runWithShellPermissionIdentity(() -> {
                 mCmShim.registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
                 mCmShim.registerDefaultNetworkCallbackForUid(Process.myUid(), perUidCallback, h);
             }, NETWORK_SETTINGS);
+            mCm.registerBestMatchingNetworkCallback(makeDefaultRequest(), bestMatchingCallback, h);
         }
 
         Network wifiNetwork = null;
@@ -635,6 +778,10 @@
                 assertNotNull("Did not receive onAvailable on per-UID default network callback",
                         perUidNetwork);
                 assertEquals(defaultNetwork, perUidNetwork);
+                final Network bestMatchingNetwork = bestMatchingCallback.waitForAvailable();
+                assertNotNull("Did not receive onAvailable on best matching network callback",
+                        bestMatchingNetwork);
+                assertEquals(defaultNetwork, bestMatchingNetwork);
             }
 
         } catch (InterruptedException e) {
@@ -645,6 +792,7 @@
             if (TestUtils.shouldTestSApis()) {
                 mCm.unregisterNetworkCallback(systemDefaultCallback);
                 mCm.unregisterNetworkCallback(perUidCallback);
+                mCm.unregisterNetworkCallback(bestMatchingCallback);
             }
         }
     }
@@ -657,10 +805,7 @@
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
     public void testRegisterNetworkCallback_withPendingIntent() {
-        if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
-            Log.i(TAG, "testRegisterNetworkCallback cannot execute unless device supports WiFi");
-            return;
-        }
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
         // Create a ConnectivityActionReceiver that has an IntentFilter for our locally defined
         // action, NETWORK_CALLBACK_ACTION.
@@ -831,7 +976,8 @@
         }
     }
 
-    private void waitForActiveNetworkMetered(int targetTransportType, boolean requestedMeteredness)
+    private void waitForActiveNetworkMetered(final int targetTransportType,
+            final boolean requestedMeteredness, final boolean useSystemDefault)
             throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
         final NetworkCallback networkCallback = new NetworkCallback() {
@@ -845,17 +991,36 @@
                 }
             }
         };
-        // Registering a callback here guarantees onCapabilitiesChanged is called immediately
-        // with the current setting. Therefore, if the setting has already been changed,
-        // this method will return right away, and if not it will wait for the setting to change.
-        mCm.registerDefaultNetworkCallback(networkCallback);
-        // Changing meteredness on wifi involves reconnecting, which can take several seconds
-        // (involves re-associating, DHCP...).
-        if (!latch.await(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
-            fail("Timed out waiting for active network metered status to change to "
-                 + requestedMeteredness + " ; network = " + mCm.getActiveNetwork());
+
+        try {
+            // Registering a callback here guarantees onCapabilitiesChanged is called immediately
+            // with the current setting. Therefore, if the setting has already been changed,
+            // this method will return right away, and if not, it'll wait for the setting to change.
+            if (useSystemDefault) {
+                runWithShellPermissionIdentity(() ->
+                                mCmShim.registerSystemDefaultNetworkCallback(networkCallback,
+                                        new Handler(Looper.getMainLooper())),
+                        NETWORK_SETTINGS);
+            } else {
+                mCm.registerDefaultNetworkCallback(networkCallback);
+            }
+
+            // Changing meteredness on wifi involves reconnecting, which can take several seconds
+            // (involves re-associating, DHCP...).
+            if (!latch.await(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+                fail("Timed out waiting for active network metered status to change to "
+                        + requestedMeteredness + " ; network = " + mCm.getActiveNetwork());
+            }
+        } finally {
+            mCm.unregisterNetworkCallback(networkCallback);
         }
-        mCm.unregisterNetworkCallback(networkCallback);
+    }
+
+    private void setWifiMeteredStatusAndWait(String ssid, boolean isMetered) throws Exception {
+        setWifiMeteredStatus(ssid, Boolean.toString(isMetered) /* metered */);
+        waitForActiveNetworkMetered(TRANSPORT_WIFI,
+                isMetered /* requestedMeteredness */,
+                true /* useSystemDefault */);
     }
 
     private void assertMultipathPreferenceIsEventually(Network network, int oldValue,
@@ -917,10 +1082,9 @@
             int newMeteredPreference = findNextPrefValue(resolver);
             Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
                     Integer.toString(newMeteredPreference));
-            setWifiMeteredStatus(ssid, "true");
-            waitForActiveNetworkMetered(TRANSPORT_WIFI, true);
             // Wifi meterness changes from unmetered to metered will disconnect and reconnect since
             // R.
+            setWifiMeteredStatusAndWait(ssid, true);
             final Network network = mCtsNetUtils.ensureWifiConnected();
             assertEquals(ssid, unquoteSSID(mWifiManager.getConnectionInfo().getSSID()));
             assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
@@ -937,9 +1101,8 @@
             assertMultipathPreferenceIsEventually(network,
                     oldMeteredPreference, newMeteredPreference);
 
-            setWifiMeteredStatus(ssid, "false");
             // No disconnect from unmetered to metered.
-            waitForActiveNetworkMetered(TRANSPORT_WIFI, false);
+            setWifiMeteredStatusAndWait(ssid, false);
             assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
                     NET_CAPABILITY_NOT_METERED), true);
             assertMultipathPreferenceIsEventually(network, newMeteredPreference,
@@ -1129,11 +1292,7 @@
     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
     @Test
     public void testKeepaliveWifiUnsupported() throws Exception {
-        if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
-            Log.i(TAG, "testKeepaliveUnsupported cannot execute unless device"
-                    + " supports WiFi");
-            return;
-        }
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
         final Network network = mCtsNetUtils.ensureWifiConnected();
         if (getSupportedKeepalivesForNet(network) != 0) return;
@@ -1150,10 +1309,7 @@
     @Test
     @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
     public void testCreateTcpKeepalive() throws Exception {
-        if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
-            Log.i(TAG, "testCreateTcpKeepalive cannot execute unless device supports WiFi");
-            return;
-        }
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
         final Network network = mCtsNetUtils.ensureWifiConnected();
         if (getSupportedKeepalivesForNet(network) == 0) return;
@@ -1360,11 +1516,7 @@
     @Test
     @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
     public void testSocketKeepaliveLimitWifi() throws Exception {
-        if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
-            Log.i(TAG, "testSocketKeepaliveLimitWifi cannot execute unless device"
-                    + " supports WiFi");
-            return;
-        }
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
         final Network network = mCtsNetUtils.ensureWifiConnected();
         final int supported = getSupportedKeepalivesForNet(network);
@@ -1457,11 +1609,7 @@
     @Test
     @SkipPresubmit(reason = "Keepalive is not supported on virtual hardware")
     public void testSocketKeepaliveUnprivileged() throws Exception {
-        if (!mPackageManager.hasSystemFeature(FEATURE_WIFI)) {
-            Log.i(TAG, "testSocketKeepaliveUnprivileged cannot execute unless device"
-                    + " supports WiFi");
-            return;
-        }
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
 
         final Network network = mCtsNetUtils.ensureWifiConnected();
         final int supported = getSupportedKeepalivesForNet(network);
@@ -1608,6 +1756,24 @@
                 c -> c instanceof CallbackEntry.Available);
     }
 
+    private void waitForAvailable(
+            @NonNull final TestableNetworkCallback cb, final int expectedTransport) {
+        cb.eventuallyExpect(
+                CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
+                entry -> {
+                    final NetworkCapabilities nc = mCm.getNetworkCapabilities(entry.getNetwork());
+                    return nc.hasTransport(expectedTransport);
+                }
+        );
+    }
+
+    private void waitForAvailable(
+            @NonNull final TestableNetworkCallback cb, @NonNull final Network expectedNetwork) {
+        cb.expectAvailableCallbacks(expectedNetwork, false /* suspended */,
+                true /* validated */,
+                false /* blocked */, NETWORK_CALLBACK_TIMEOUT_MS);
+    }
+
     private void waitForLost(@NonNull final TestableNetworkCallback cb) {
         cb.eventuallyExpect(CallbackEntry.LOST, NETWORK_CALLBACK_TIMEOUT_MS,
                 c -> c instanceof CallbackEntry.Lost);
@@ -1717,6 +1883,7 @@
      * Verify background request can only be requested when acquiring
      * {@link android.Manifest.permission.NETWORK_SETTINGS}.
      */
+    @AppModeFull(reason = "Instant apps cannot create test networks")
     @Test
     public void testRequestBackgroundNetwork() {
         // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
@@ -1912,4 +2079,552 @@
         assertNull(NetworkInformationShimImpl.newInstance()
                 .getCapabilityCarrierName(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
     }
+
+    @Test
+    public void testSetGlobalProxy() {
+        assumeTrue(TestUtils.shouldTestSApis());
+        // Behavior is verified in gts. Verify exception thrown w/o permission.
+        assertThrows(SecurityException.class, () -> mCm.setGlobalProxy(
+                ProxyInfo.buildDirectProxy("example.com" /* host */, 8080 /* port */)));
+    }
+
+    @Test
+    public void testFactoryResetWithoutPermission() {
+        assumeTrue(TestUtils.shouldTestSApis());
+        assertThrows(SecurityException.class, () -> mCm.factoryReset());
+    }
+
+    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
+    public void testFactoryReset() throws Exception {
+        assumeTrue(TestUtils.shouldTestSApis());
+
+        // Store current settings.
+        final int curAvoidBadWifi =
+                ConnectivitySettingsManager.getNetworkAvoidBadWifi(mContext);
+        final int curPrivateDnsMode = ConnectivitySettingsManager.getPrivateDnsMode(mContext);
+
+        final TestTetheringEventCallback tetherEventCallback = new TestTetheringEventCallback();
+        try {
+            mTm.registerTetheringEventCallback(c -> c.run() /* executor */, tetherEventCallback);
+            // Adopt for NETWORK_SETTINGS permission.
+            mUiAutomation.adoptShellPermissionIdentity();
+            // start tethering
+            tetherEventCallback.assumeWifiTetheringSupported(mContext);
+            startWifiTethering(tetherEventCallback);
+            // Update setting to verify the behavior.
+            mCm.setAirplaneMode(true);
+            ConnectivitySettingsManager.setPrivateDnsMode(mContext,
+                    ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF);
+            ConnectivitySettingsManager.setNetworkAvoidBadWifi(mContext,
+                    ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI_IGNORE);
+            assertEquals(AIRPLANE_MODE_ON, Settings.Global.getInt(
+                    mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON));
+            // Verify factoryReset
+            mCm.factoryReset();
+            verifySettings(AIRPLANE_MODE_OFF,
+                    ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC,
+                    ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI_PROMPT);
+
+            tetherEventCallback.expectNoTetheringActive();
+        } finally {
+            // Restore settings.
+            mCm.setAirplaneMode(false);
+            ConnectivitySettingsManager.setNetworkAvoidBadWifi(mContext, curAvoidBadWifi);
+            ConnectivitySettingsManager.setPrivateDnsMode(mContext, curPrivateDnsMode);
+            mTm.unregisterTetheringEventCallback(tetherEventCallback);
+            mTm.stopAllTethering();
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    /**
+     * Verify that {@link ConnectivityManager#setProfileNetworkPreference} cannot be called
+     * without required NETWORK_STACK permissions.
+     */
+    @Test
+    public void testSetProfileNetworkPreference_NoPermission() {
+        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+        // shims, and @IgnoreUpTo does not check that.
+        assumeTrue(TestUtils.shouldTestSApis());
+        assertThrows(SecurityException.class, () -> mCm.setProfileNetworkPreference(
+                UserHandle.of(0), PROFILE_NETWORK_PREFERENCE_ENTERPRISE, null /* executor */,
+                null /* listener */));
+    }
+
+    @Test
+    public void testSystemReady() {
+        assumeTrue(TestUtils.shouldTestSApis());
+        assertThrows(SecurityException.class, () -> mCm.systemReady());
+    }
+
+    @Test
+    public void testGetIpSecNetIdRange() {
+        assumeTrue(TestUtils.shouldTestSApis());
+        // The lower refers to ConnectivityManager.TUN_INTF_NETID_START.
+        final long lower = 64512;
+        // The upper refers to ConnectivityManager.TUN_INTF_NETID_START
+        // + ConnectivityManager.TUN_INTF_NETID_RANGE - 1
+        final long upper = 65535;
+        assertEquals(lower, (long) ConnectivityManager.getIpSecNetIdRange().getLower());
+        assertEquals(upper, (long) ConnectivityManager.getIpSecNetIdRange().getUpper());
+    }
+
+    private void verifySettings(int expectedAirplaneMode, int expectedPrivateDnsMode,
+            int expectedAvoidBadWifi) throws Exception {
+        assertEquals(expectedAirplaneMode, Settings.Global.getInt(
+                mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON));
+        assertEquals(expectedPrivateDnsMode,
+                ConnectivitySettingsManager.getPrivateDnsMode(mContext));
+        assertEquals(expectedAvoidBadWifi,
+                ConnectivitySettingsManager.getNetworkAvoidBadWifi(mContext));
+    }
+
+    private void startWifiTethering(final TestTetheringEventCallback callback) throws Exception {
+        if (!isWifiTetheringSupported(mContext, callback)) return;
+
+        final List<String> wifiRegexs =
+                callback.getTetheringInterfaceRegexps().getTetherableWifiRegexs();
+        final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
+        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+                .setShouldShowEntitlementUi(false).build();
+        mTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
+        startTetheringCallback.verifyTetheringStarted();
+        callback.expectTetheredInterfacesChanged(wifiRegexs, TETHERING_WIFI);
+    }
+
+    /**
+     * Verify that per-app OEM network preference functions as expected for network preference TEST.
+     * For specified apps, validate networks are prioritized in order: unmetered, TEST transport,
+     * default network.
+     */
+    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
+    public void testSetOemNetworkPreferenceForTestPref() throws Exception {
+        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+        // shims, and @IgnoreUpTo does not check that.
+        assumeTrue(TestUtils.shouldTestSApis());
+        assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
+
+        final TestNetworkTracker tnt = callWithShellPermissionIdentity(
+                () -> initTestNetwork(mContext, TEST_LINKADDR, NETWORK_CALLBACK_TIMEOUT_MS));
+        final TestableNetworkCallback defaultCallback = new TestableNetworkCallback();
+        final TestableNetworkCallback systemDefaultCallback = new TestableNetworkCallback();
+
+        final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
+        final NetworkCapabilities wifiNetworkCapabilities = callWithShellPermissionIdentity(
+                () -> mCm.getNetworkCapabilities(wifiNetwork));
+        final String ssid = unquoteSSID(wifiNetworkCapabilities.getSsid());
+        final boolean oldMeteredValue = wifiNetworkCapabilities.isMetered();
+
+        try {
+            // This network will be used for unmetered.
+            setWifiMeteredStatusAndWait(ssid, false /* isMetered */);
+
+            setOemNetworkPreferenceForMyPackage(OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST);
+            registerTestOemNetworkPreferenceCallbacks(defaultCallback, systemDefaultCallback);
+
+            // Validate that an unmetered network is used over other networks.
+            waitForAvailable(defaultCallback, wifiNetwork);
+            waitForAvailable(systemDefaultCallback, wifiNetwork);
+
+            // Validate when setting unmetered to metered, unmetered is lost and replaced by the
+            // network with the TEST transport.
+            setWifiMeteredStatusAndWait(ssid, true /* isMetered */);
+            defaultCallback.expectCallback(CallbackEntry.LOST, wifiNetwork,
+                    NETWORK_CALLBACK_TIMEOUT_MS);
+            waitForAvailable(defaultCallback, tnt.getNetwork());
+            // Depending on if this device has cellular connectivity or not, multiple available
+            // callbacks may be received. Eventually, metered Wi-Fi should be the final available
+            // callback in any case therefore confirm its receipt before continuing to assure the
+            // system is in the expected state.
+            waitForAvailable(systemDefaultCallback, TRANSPORT_WIFI);
+        } finally {
+            // Validate that removing the test network will fallback to the default network.
+            runWithShellPermissionIdentity(tnt::teardown);
+            defaultCallback.expectCallback(CallbackEntry.LOST, tnt.getNetwork(),
+                    NETWORK_CALLBACK_TIMEOUT_MS);
+            waitForAvailable(defaultCallback);
+
+            setWifiMeteredStatusAndWait(ssid, oldMeteredValue);
+
+            // Cleanup any prior test state from setOemNetworkPreference
+            clearOemNetworkPreference();
+            unregisterTestOemNetworkPreferenceCallbacks(defaultCallback, systemDefaultCallback);
+        }
+    }
+
+    /**
+     * Verify that per-app OEM network preference functions as expected for network pref TEST_ONLY.
+     * For specified apps, validate that only TEST transport type networks are used.
+     */
+    @Test
+    public void testSetOemNetworkPreferenceForTestOnlyPref() throws Exception {
+        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+        // shims, and @IgnoreUpTo does not check that.
+        assumeTrue(TestUtils.shouldTestSApis());
+
+        final TestNetworkTracker tnt = callWithShellPermissionIdentity(
+                () -> initTestNetwork(mContext, TEST_LINKADDR, NETWORK_CALLBACK_TIMEOUT_MS));
+        final TestableNetworkCallback defaultCallback = new TestableNetworkCallback();
+        final TestableNetworkCallback systemDefaultCallback = new TestableNetworkCallback();
+
+        final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
+
+        try {
+            setOemNetworkPreferenceForMyPackage(
+                    OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY);
+            registerTestOemNetworkPreferenceCallbacks(defaultCallback, systemDefaultCallback);
+            waitForAvailable(defaultCallback, tnt.getNetwork());
+            waitForAvailable(systemDefaultCallback, wifiNetwork);
+        } finally {
+            runWithShellPermissionIdentity(tnt::teardown);
+            defaultCallback.expectCallback(CallbackEntry.LOST, tnt.getNetwork(),
+                    NETWORK_CALLBACK_TIMEOUT_MS);
+
+            // This network preference should only ever use the test network therefore available
+            // should not trigger when the test network goes down (e.g. switch to cellular).
+            defaultCallback.assertNoCallback();
+            // The system default should still be connected to Wi-fi
+            assertEquals(wifiNetwork, systemDefaultCallback.getLastAvailableNetwork());
+
+            // Cleanup any prior test state from setOemNetworkPreference
+            clearOemNetworkPreference();
+
+            // The default (non-test) network should be available as the network pref was cleared.
+            waitForAvailable(defaultCallback);
+            unregisterTestOemNetworkPreferenceCallbacks(defaultCallback, systemDefaultCallback);
+        }
+    }
+
+    private void unregisterTestOemNetworkPreferenceCallbacks(
+            @NonNull final TestableNetworkCallback defaultCallback,
+            @NonNull final TestableNetworkCallback systemDefaultCallback) {
+        mCm.unregisterNetworkCallback(defaultCallback);
+        mCm.unregisterNetworkCallback(systemDefaultCallback);
+    }
+
+    private void registerTestOemNetworkPreferenceCallbacks(
+            @NonNull final TestableNetworkCallback defaultCallback,
+            @NonNull final TestableNetworkCallback systemDefaultCallback) {
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+        runWithShellPermissionIdentity(() ->
+                mCmShim.registerSystemDefaultNetworkCallback(systemDefaultCallback,
+                        new Handler(Looper.getMainLooper())), NETWORK_SETTINGS);
+    }
+
+    private static final class OnCompleteListenerCallback {
+        final CompletableFuture<Object> mDone = new CompletableFuture<>();
+
+        public void onComplete() {
+            mDone.complete(new Object());
+        }
+
+        void expectOnComplete() throws Exception {
+            try {
+                mDone.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            } catch (TimeoutException e) {
+                fail("Expected onComplete() not received after "
+                        + NETWORK_CALLBACK_TIMEOUT_MS + " ms");
+            }
+        }
+    }
+
+    private void setOemNetworkPreferenceForMyPackage(final int networkPref) throws Exception {
+        final OemNetworkPreferences pref = new OemNetworkPreferences.Builder()
+                .addNetworkPreference(mContext.getPackageName(), networkPref)
+                .build();
+        final OnCompleteListenerCallback oemPrefListener = new OnCompleteListenerCallback();
+        mUiAutomation.adoptShellPermissionIdentity();
+        try {
+            mCm.setOemNetworkPreference(
+                    pref, mContext.getMainExecutor(), oemPrefListener::onComplete);
+        } finally {
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+        oemPrefListener.expectOnComplete();
+    }
+
+    /**
+     * This will clear the OEM network preference on the device. As there is currently no way of
+     * getting the existing preference, if this is executed while an existing preference is in
+     * place, that preference will need to be reapplied after executing this test.
+     * @throws Exception
+     */
+    private void clearOemNetworkPreference() throws Exception {
+        final OemNetworkPreferences clearPref = new OemNetworkPreferences.Builder().build();
+        final OnCompleteListenerCallback oemPrefListener = new OnCompleteListenerCallback();
+        mUiAutomation.adoptShellPermissionIdentity();
+        try {
+            mCm.setOemNetworkPreference(
+                    clearPref, mContext.getMainExecutor(), oemPrefListener::onComplete);
+        } finally {
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+        oemPrefListener.expectOnComplete();
+    }
+
+    @Test
+    public void testSetAcceptPartialConnectivity_NoPermission_GetException() {
+        assumeTrue(TestUtils.shouldTestSApis());
+        assertThrows(SecurityException.class, () -> mCm.setAcceptPartialConnectivity(
+                mCm.getActiveNetwork(), false /* accept */ , false /* always */));
+    }
+
+    @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
+    @Test
+    public void testAcceptPartialConnectivity_validatedNetwork() throws Exception {
+        assumeTrue(TestUtils.shouldTestSApis());
+        assumeTrue("testAcceptPartialConnectivity_validatedNetwork cannot execute"
+                        + " unless device supports WiFi",
+                mPackageManager.hasSystemFeature(FEATURE_WIFI));
+
+        try {
+            // Wait for partial connectivity to be detected on the network
+            final Network network = preparePartialConnectivity();
+
+            runAsShell(NETWORK_SETTINGS, () -> {
+                // The always bit is verified in NetworkAgentTest
+                mCm.setAcceptPartialConnectivity(network, true /* accept */, false /* always */);
+            });
+
+            // Accept partial connectivity network should result in a validated network
+            expectNetworkHasCapability(network, NET_CAPABILITY_VALIDATED, WIFI_CONNECT_TIMEOUT_MS);
+        } finally {
+            resetValidationConfig();
+            // Reconnect wifi to reset the wifi status
+            mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
+            mCtsNetUtils.ensureWifiConnected();
+        }
+    }
+
+    @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
+    @Test
+    public void testRejectPartialConnectivity_TearDownNetwork() throws Exception {
+        assumeTrue(TestUtils.shouldTestSApis());
+        assumeTrue("testAcceptPartialConnectivity_validatedNetwork cannot execute"
+                        + " unless device supports WiFi",
+                mPackageManager.hasSystemFeature(FEATURE_WIFI));
+
+        final TestNetworkCallback cb = new TestNetworkCallback();
+        try {
+            // Wait for partial connectivity to be detected on the network
+            final Network network = preparePartialConnectivity();
+
+            mCm.requestNetwork(makeWifiNetworkRequest(), cb);
+            runAsShell(NETWORK_SETTINGS, () -> {
+                // The always bit is verified in NetworkAgentTest
+                mCm.setAcceptPartialConnectivity(network, false /* accept */, false /* always */);
+            });
+            // Reject partial connectivity network should cause the network being torn down
+            assertEquals(network, cb.waitForLost());
+        } finally {
+            mCm.unregisterNetworkCallback(cb);
+            resetValidationConfig();
+            // Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
+            // apply here. Thus, turn off wifi first and restart to restore.
+            runShellCommand("svc wifi disable");
+            mCtsNetUtils.ensureWifiConnected();
+        }
+    }
+
+    @Test
+    public void testSetAcceptUnvalidated_NoPermission_GetException() {
+        assumeTrue(TestUtils.shouldTestSApis());
+        assertThrows(SecurityException.class, () -> mCm.setAcceptUnvalidated(
+                mCm.getActiveNetwork(), false /* accept */ , false /* always */));
+    }
+
+    @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
+    @Test
+    public void testRejectUnvalidated_TearDownNetwork() throws Exception {
+        assumeTrue(TestUtils.shouldTestSApis());
+        final boolean canRunTest = mPackageManager.hasSystemFeature(FEATURE_WIFI)
+                && mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
+        assumeTrue("testAcceptPartialConnectivity_validatedNetwork cannot execute"
+                        + " unless device supports WiFi and telephony", canRunTest);
+
+        final TestableNetworkCallback wifiCb = new TestableNetworkCallback();
+        try {
+            // Ensure at least one default network candidate connected.
+            mCtsNetUtils.connectToCell();
+
+            final Network wifiNetwork = prepareUnvalidatedNetwork();
+            // Default network should not be wifi ,but checking that wifi is not the default doesn't
+            // guarantee that it won't become the default in the future.
+            assertNotEquals(wifiNetwork, mCm.getActiveNetwork());
+
+            mCm.registerNetworkCallback(makeWifiNetworkRequest(), wifiCb);
+            runAsShell(NETWORK_SETTINGS, () -> {
+                mCm.setAcceptUnvalidated(wifiNetwork, false /* accept */, false /* always */);
+            });
+            waitForLost(wifiCb);
+        } finally {
+            mCm.unregisterNetworkCallback(wifiCb);
+            resetValidationConfig();
+            /// Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
+            // apply here. Thus, turn off wifi first and restart to restore.
+            runShellCommand("svc wifi disable");
+            mCtsNetUtils.ensureWifiConnected();
+        }
+    }
+
+    private Network expectNetworkHasCapability(Network network, int expectedNetCap, long timeout)
+            throws Exception {
+        final CompletableFuture<Network> future = new CompletableFuture();
+        final NetworkCallback cb = new NetworkCallback() {
+            @Override
+            public void onCapabilitiesChanged(Network n, NetworkCapabilities nc) {
+                if (n.equals(network) && nc.hasCapability(expectedNetCap)) {
+                    future.complete(network);
+                }
+            }
+        };
+
+        try {
+            mCm.registerNetworkCallback(new NetworkRequest.Builder().build(), cb);
+            return future.get(timeout, TimeUnit.MILLISECONDS);
+        } finally {
+            mCm.unregisterNetworkCallback(cb);
+        }
+    }
+
+    private void resetValidationConfig() {
+        NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig();
+        mHttpServer.stop();
+    }
+
+    private void prepareHttpServer() throws Exception {
+        runAsShell(READ_DEVICE_CONFIG, () -> {
+            // Verify that the test URLs are not normally set on the device, but do not fail if the
+            // test URLs are set to what this test uses (URLs on localhost), in case the test was
+            // interrupted manually and rerun.
+            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL);
+            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL);
+        });
+
+        NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig();
+
+        mHttpServer.start();
+    }
+
+    private Network preparePartialConnectivity() throws Exception {
+        prepareHttpServer();
+        // Configure response code for partial connectivity
+        configTestServer(Status.INTERNAL_ERROR  /* httpsStatusCode */,
+                Status.NO_CONTENT  /* httpStatusCode */);
+        // Disconnect wifi first then start wifi network with configuration.
+        mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
+        final Network network = mCtsNetUtils.ensureWifiConnected();
+
+        return expectNetworkHasCapability(network, NET_CAPABILITY_PARTIAL_CONNECTIVITY,
+                WIFI_CONNECT_TIMEOUT_MS);
+    }
+
+    private Network prepareUnvalidatedNetwork() throws Exception {
+        prepareHttpServer();
+        // Configure response code for unvalidated network
+        configTestServer(Status.INTERNAL_ERROR /* httpsStatusCode */,
+                Status.INTERNAL_ERROR /* httpStatusCode */);
+
+        // Disconnect wifi first then start wifi network with configuration.
+        mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
+        final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
+        return expectNetworkHasCapability(wifiNetwork, NET_CAPABILITY_INTERNET,
+                WIFI_CONNECT_TIMEOUT_MS);
+    }
+
+    private String makeUrl(String path) {
+        return "http://localhost:" + mHttpServer.getListeningPort() + path;
+    }
+
+    private void assertEmptyOrLocalhostUrl(String urlKey) {
+        final String url = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONNECTIVITY, urlKey);
+        assertTrue(urlKey + " must not be set in production scenarios, current value= " + url,
+                TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME.equals(Uri.parse(url).getHost()));
+    }
+
+    private void configTestServer(IStatus httpsStatusCode, IStatus httpStatusCode) {
+        mHttpServer.addResponse(new TestHttpServer.Request(
+                TEST_HTTPS_URL_PATH, Method.GET, "" /* queryParameters */),
+                httpsStatusCode, null /* locationHeader */, "" /* content */);
+        mHttpServer.addResponse(new TestHttpServer.Request(
+                TEST_HTTP_URL_PATH, Method.GET, "" /* queryParameters */),
+                httpStatusCode, null /* locationHeader */, "" /* content */);
+        NetworkValidationTestUtil.setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH));
+        NetworkValidationTestUtil.setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH));
+        NetworkValidationTestUtil.setUrlExpirationDeviceConfig(
+                System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS);
+    }
+
+    @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+    @Test
+    public void testMobileDataPreferredUids() throws Exception {
+        assumeTrue(TestUtils.shouldTestSApis());
+        final boolean canRunTest = mPackageManager.hasSystemFeature(FEATURE_WIFI)
+                && mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
+        assumeTrue("testMobileDataPreferredUidsWithCallback cannot execute"
+                + " unless device supports both WiFi and telephony", canRunTest);
+
+        final int uid = mPackageManager.getPackageUid(mContext.getPackageName(), 0 /* flag */);
+        final Set<Integer> mobileDataPreferredUids =
+                ConnectivitySettingsManager.getMobileDataPreferredUids(mContext);
+        // CtsNetTestCases uid should not list in MOBILE_DATA_PREFERRED_UIDS setting because it just
+        // installs to device. In case the uid is existed in setting mistakenly, try to remove the
+        // uid and set correct uids to setting.
+        mobileDataPreferredUids.remove(uid);
+        ConnectivitySettingsManager.setMobileDataPreferredUids(mContext, mobileDataPreferredUids);
+
+        // For testing mobile data preferred uids feature, it needs both wifi and cell network.
+        final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
+        final Network cellNetwork = mCtsNetUtils.connectToCell();
+        final TestableNetworkCallback defaultTrackingCb = new TestableNetworkCallback();
+        final TestableNetworkCallback systemDefaultCb = new TestableNetworkCallback();
+        final Handler h = new Handler(Looper.getMainLooper());
+        runWithShellPermissionIdentity(() -> mCm.registerSystemDefaultNetworkCallback(
+                systemDefaultCb, h), NETWORK_SETTINGS);
+        mCm.registerDefaultNetworkCallback(defaultTrackingCb);
+
+        try {
+            // CtsNetTestCases uid is not listed in MOBILE_DATA_PREFERRED_UIDS setting, so the
+            // per-app default network should be same as system default network.
+            waitForAvailable(systemDefaultCb, wifiNetwork);
+            defaultTrackingCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
+                    entry -> wifiNetwork.equals(entry.getNetwork()));
+            // Active network for CtsNetTestCases uid should be wifi now.
+            assertEquals(wifiNetwork, mCm.getActiveNetwork());
+
+            // Add CtsNetTestCases uid to MOBILE_DATA_PREFERRED_UIDS setting, then available per-app
+            // default network callback should be received with cell network.
+            final Set<Integer> newMobileDataPreferredUids = new ArraySet<>(mobileDataPreferredUids);
+            newMobileDataPreferredUids.add(uid);
+            ConnectivitySettingsManager.setMobileDataPreferredUids(
+                    mContext, newMobileDataPreferredUids);
+            defaultTrackingCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
+                    entry -> cellNetwork.equals(entry.getNetwork()));
+            // System default network doesn't change.
+            systemDefaultCb.assertNoCallback();
+            // Active network for CtsNetTestCases uid should change to cell, too.
+            assertEquals(cellNetwork, mCm.getActiveNetwork());
+
+            // Remove CtsNetTestCases uid from MOBILE_DATA_PREFERRED_UIDS setting, then available
+            // per-app default network callback should be received again with system default network
+            newMobileDataPreferredUids.remove(uid);
+            ConnectivitySettingsManager.setMobileDataPreferredUids(
+                    mContext, newMobileDataPreferredUids);
+            defaultTrackingCb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
+                    entry -> wifiNetwork.equals(entry.getNetwork()));
+            // System default network still doesn't change.
+            systemDefaultCb.assertNoCallback();
+            // Active network for CtsNetTestCases uid should change back to wifi.
+            assertEquals(wifiNetwork, mCm.getActiveNetwork());
+        } finally {
+            mCm.unregisterNetworkCallback(systemDefaultCb);
+            mCm.unregisterNetworkCallback(defaultTrackingCb);
+
+            // Restore setting.
+            ConnectivitySettingsManager.setMobileDataPreferredUids(
+                    mContext, mobileDataPreferredUids);
+        }
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index c6d8d65..6e9f0cd 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -39,13 +39,11 @@
 import android.net.ConnectivityManager;
 import android.net.Ikev2VpnProfile;
 import android.net.IpSecAlgorithm;
-import android.net.LinkAddress;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.ProxyInfo;
 import android.net.TestNetworkInterface;
-import android.net.TestNetworkManager;
 import android.net.VpnManager;
 import android.net.cts.util.CtsNetUtils;
 import android.os.Build;
@@ -439,41 +437,37 @@
         assertEquals(vpnNetwork, cb.lastLostNetwork);
     }
 
-    private void doTestStartStopVpnProfile(boolean testIpv6) throws Exception {
-        // Non-final; these variables ensure we clean up properly after our test if we have
-        // allocated test network resources
-        final TestNetworkManager tnm = sContext.getSystemService(TestNetworkManager.class);
-        TestNetworkInterface testIface = null;
-        TestNetworkCallback tunNetworkCallback = null;
+    private class VerifyStartStopVpnProfileTest implements TestNetworkRunnable.Test {
+        private final boolean mTestIpv6Only;
 
-        try {
-            // Build underlying test network
-            testIface = tnm.createTunInterface(
-                    new LinkAddress[] {
-                            new LinkAddress(LOCAL_OUTER_4, IP4_PREFIX_LEN),
-                            new LinkAddress(LOCAL_OUTER_6, IP6_PREFIX_LEN)});
+        /**
+         * Constructs the test
+         *
+         * @param testIpv6Only if true, builds a IPv6-only test; otherwise builds a IPv4-only test
+         */
+        VerifyStartStopVpnProfileTest(boolean testIpv6Only) {
+            mTestIpv6Only = testIpv6Only;
+        }
 
-            // Hold on to this callback to ensure network does not get reaped.
-            tunNetworkCallback = mCtsNetUtils.setupAndGetTestNetwork(
-                    testIface.getInterfaceName());
+        @Override
+        public void runTest(TestNetworkInterface testIface, TestNetworkCallback tunNetworkCallback)
+                throws Exception {
             final IkeTunUtils tunUtils = new IkeTunUtils(testIface.getFileDescriptor());
 
-            checkStartStopVpnProfileBuildsNetworks(tunUtils, testIpv6);
-        } finally {
-            // Make sure to stop the VPN profile. This is safe to call multiple times.
+            checkStartStopVpnProfileBuildsNetworks(tunUtils, mTestIpv6Only);
+        }
+
+        @Override
+        public void cleanupTest() {
             sVpnMgr.stopProvisionedVpnProfile();
+        }
 
-            if (testIface != null) {
-                testIface.getFileDescriptor().close();
-            }
-
-            if (tunNetworkCallback != null) {
-                sCM.unregisterNetworkCallback(tunNetworkCallback);
-            }
-
-            final Network testNetwork = tunNetworkCallback.currentNetwork;
-            if (testNetwork != null) {
-                tnm.teardownTestNetwork(testNetwork);
+        @Override
+        public InetAddress[] getTestNetworkAddresses() {
+            if (mTestIpv6Only) {
+                return new InetAddress[] {LOCAL_OUTER_6};
+            } else {
+                return new InetAddress[] {LOCAL_OUTER_4};
             }
         }
     }
@@ -483,9 +477,8 @@
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
 
         // Requires shell permission to update appops.
-        runWithShellPermissionIdentity(() -> {
-            doTestStartStopVpnProfile(false);
-        });
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(false)));
     }
 
     @Test
@@ -493,9 +486,8 @@
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
 
         // Requires shell permission to update appops.
-        runWithShellPermissionIdentity(() -> {
-            doTestStartStopVpnProfile(true);
-        });
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(true)));
     }
 
     private static class CertificateAndKey {
diff --git a/tests/cts/net/src/android/net/cts/IpSecAlgorithmImplTest.java b/tests/cts/net/src/android/net/cts/IpSecAlgorithmImplTest.java
new file mode 100644
index 0000000..2f29273
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/IpSecAlgorithmImplTest.java
@@ -0,0 +1,306 @@
+/*
+ * 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.cts;
+
+import static android.net.IpSecAlgorithm.AUTH_AES_CMAC;
+import static android.net.IpSecAlgorithm.AUTH_AES_XCBC;
+import static android.net.IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305;
+import static android.net.IpSecAlgorithm.CRYPT_AES_CTR;
+import static android.net.cts.PacketUtils.AES_CMAC;
+import static android.net.cts.PacketUtils.AES_CMAC_ICV_LEN;
+import static android.net.cts.PacketUtils.AES_CMAC_KEY_LEN;
+import static android.net.cts.PacketUtils.AES_CTR;
+import static android.net.cts.PacketUtils.AES_CTR_BLK_SIZE;
+import static android.net.cts.PacketUtils.AES_CTR_IV_LEN;
+import static android.net.cts.PacketUtils.AES_CTR_KEY_LEN_20;
+import static android.net.cts.PacketUtils.AES_CTR_KEY_LEN_28;
+import static android.net.cts.PacketUtils.AES_CTR_KEY_LEN_36;
+import static android.net.cts.PacketUtils.AES_CTR_SALT_LEN;
+import static android.net.cts.PacketUtils.AES_XCBC;
+import static android.net.cts.PacketUtils.AES_XCBC_ICV_LEN;
+import static android.net.cts.PacketUtils.AES_XCBC_KEY_LEN;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_BLK_SIZE;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_ICV_LEN;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_IV_LEN;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_KEY_LEN;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_SALT_LEN;
+import static android.net.cts.PacketUtils.ESP_HDRLEN;
+import static android.net.cts.PacketUtils.IP6_HDRLEN;
+import static android.net.cts.PacketUtils.getIpHeader;
+import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assume.assumeTrue;
+
+import android.net.IpSecAlgorithm;
+import android.net.IpSecManager;
+import android.net.IpSecTransform;
+import android.net.Network;
+import android.net.TestNetworkInterface;
+import android.net.cts.PacketUtils.BytePayload;
+import android.net.cts.PacketUtils.EspAeadCipher;
+import android.net.cts.PacketUtils.EspAuth;
+import android.net.cts.PacketUtils.EspAuthNull;
+import android.net.cts.PacketUtils.EspCipher;
+import android.net.cts.PacketUtils.EspCipherNull;
+import android.net.cts.PacketUtils.EspCryptCipher;
+import android.net.cts.PacketUtils.EspHeader;
+import android.net.cts.PacketUtils.IpHeader;
+import android.net.cts.PacketUtils.UdpHeader;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Socket cannot bind in instant app mode")
+public class IpSecAlgorithmImplTest extends IpSecBaseTest {
+    private static final InetAddress LOCAL_ADDRESS =
+            InetAddress.parseNumericAddress("2001:db8:1::1");
+    private static final InetAddress REMOTE_ADDRESS =
+            InetAddress.parseNumericAddress("2001:db8:1::2");
+
+    private static final int REMOTE_PORT = 12345;
+    private static final IpSecManager sIpSecManager =
+            InstrumentationRegistry.getContext().getSystemService(IpSecManager.class);
+
+    private static class CheckCryptoImplTest implements TestNetworkRunnable.Test {
+        private final IpSecAlgorithm mIpsecEncryptAlgo;
+        private final IpSecAlgorithm mIpsecAuthAlgo;
+        private final IpSecAlgorithm mIpsecAeadAlgo;
+        private final EspCipher mEspCipher;
+        private final EspAuth mEspAuth;
+
+        CheckCryptoImplTest(
+                IpSecAlgorithm ipsecEncryptAlgo,
+                IpSecAlgorithm ipsecAuthAlgo,
+                IpSecAlgorithm ipsecAeadAlgo,
+                EspCipher espCipher,
+                EspAuth espAuth) {
+            mIpsecEncryptAlgo = ipsecEncryptAlgo;
+            mIpsecAuthAlgo = ipsecAuthAlgo;
+            mIpsecAeadAlgo = ipsecAeadAlgo;
+            mEspCipher = espCipher;
+            mEspAuth = espAuth;
+        }
+
+        private static byte[] buildTransportModeEspPayload(
+                int srcPort, int dstPort, int spi, EspCipher espCipher, EspAuth espAuth)
+                throws Exception {
+            final UdpHeader udpPayload =
+                    new UdpHeader(srcPort, dstPort, new BytePayload(TEST_DATA));
+            final IpHeader preEspIpHeader =
+                    getIpHeader(
+                            udpPayload.getProtocolId(), LOCAL_ADDRESS, REMOTE_ADDRESS, udpPayload);
+
+            final PacketUtils.EspHeader espPayload =
+                    new EspHeader(
+                            udpPayload.getProtocolId(),
+                            spi,
+                            1 /* sequence number */,
+                            udpPayload.getPacketBytes(preEspIpHeader),
+                            espCipher,
+                            espAuth);
+            return espPayload.getPacketBytes(preEspIpHeader);
+        }
+
+        @Override
+        public void runTest(TestNetworkInterface testIface, TestNetworkCallback tunNetworkCallback)
+                throws Exception {
+            final TunUtils tunUtils = new TunUtils(testIface.getFileDescriptor());
+            tunNetworkCallback.waitForAvailable();
+            final Network testNetwork = tunNetworkCallback.currentNetwork;
+
+            final IpSecTransform.Builder transformBuilder =
+                    new IpSecTransform.Builder(InstrumentationRegistry.getContext());
+            if (mIpsecAeadAlgo != null) {
+                transformBuilder.setAuthenticatedEncryption(mIpsecAeadAlgo);
+            } else {
+                if (mIpsecEncryptAlgo != null) {
+                    transformBuilder.setEncryption(mIpsecEncryptAlgo);
+                }
+                if (mIpsecAuthAlgo != null) {
+                    transformBuilder.setAuthentication(mIpsecAuthAlgo);
+                }
+            }
+
+            try (IpSecManager.SecurityParameterIndex outSpi =
+                            sIpSecManager.allocateSecurityParameterIndex(REMOTE_ADDRESS);
+                    IpSecManager.SecurityParameterIndex inSpi =
+                            sIpSecManager.allocateSecurityParameterIndex(LOCAL_ADDRESS);
+                    IpSecTransform outTransform =
+                            transformBuilder.buildTransportModeTransform(LOCAL_ADDRESS, outSpi);
+                    IpSecTransform inTransform =
+                            transformBuilder.buildTransportModeTransform(REMOTE_ADDRESS, inSpi);
+                    // Bind localSocket to a random available port.
+                    DatagramSocket localSocket = new DatagramSocket(0)) {
+                sIpSecManager.applyTransportModeTransform(
+                        localSocket, IpSecManager.DIRECTION_IN, inTransform);
+                sIpSecManager.applyTransportModeTransform(
+                        localSocket, IpSecManager.DIRECTION_OUT, outTransform);
+
+                // Send ESP packet
+                final DatagramPacket outPacket =
+                        new DatagramPacket(
+                                TEST_DATA, 0, TEST_DATA.length, REMOTE_ADDRESS, REMOTE_PORT);
+                testNetwork.bindSocket(localSocket);
+                localSocket.send(outPacket);
+                final byte[] outEspPacket =
+                        tunUtils.awaitEspPacket(outSpi.getSpi(), false /* useEncap */);
+
+                // Remove transform for good hygiene
+                sIpSecManager.removeTransportModeTransforms(localSocket);
+
+                // Get the kernel-generated ESP payload
+                final byte[] outEspPayload = new byte[outEspPacket.length - IP6_HDRLEN];
+                System.arraycopy(outEspPacket, IP6_HDRLEN, outEspPayload, 0, outEspPayload.length);
+
+                // Get the IV of the kernel-generated ESP payload
+                final byte[] iv =
+                        Arrays.copyOfRange(
+                                outEspPayload, ESP_HDRLEN, ESP_HDRLEN + mEspCipher.ivLen);
+
+                // Build ESP payload using the kernel-generated IV and the user space crypto
+                // implementations
+                mEspCipher.updateIv(iv);
+                final byte[] expectedEspPayload =
+                        buildTransportModeEspPayload(
+                                localSocket.getLocalPort(),
+                                REMOTE_PORT,
+                                outSpi.getSpi(),
+                                mEspCipher,
+                                mEspAuth);
+
+                // Compare user-space-generated and kernel-generated ESP payload
+                assertArrayEquals(expectedEspPayload, outEspPayload);
+            }
+        }
+
+        @Override
+        public void cleanupTest() {
+            // Do nothing
+        }
+
+        @Override
+        public InetAddress[] getTestNetworkAddresses() {
+            return new InetAddress[] {LOCAL_ADDRESS};
+        }
+    }
+
+    private void checkAesCtr(int keyLen) throws Exception {
+        final byte[] cryptKey = getKeyBytes(keyLen);
+
+        final IpSecAlgorithm ipsecEncryptAlgo =
+                new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CTR, cryptKey);
+        final EspCipher espCipher =
+                new EspCryptCipher(
+                        AES_CTR, AES_CTR_BLK_SIZE, cryptKey, AES_CTR_IV_LEN, AES_CTR_SALT_LEN);
+
+        runWithShellPermissionIdentity(new TestNetworkRunnable(new CheckCryptoImplTest(
+                ipsecEncryptAlgo, null /* ipsecAuthAlgo */, null /* ipsecAeadAlgo */,
+                espCipher, EspAuthNull.getInstance())));
+    }
+
+    @Test
+    public void testAesCtr160() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        checkAesCtr(AES_CTR_KEY_LEN_20);
+    }
+
+    @Test
+    public void testAesCtr224() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        checkAesCtr(AES_CTR_KEY_LEN_28);
+    }
+
+    @Test
+    public void testAesCtr288() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        checkAesCtr(AES_CTR_KEY_LEN_36);
+    }
+
+    @Test
+    public void testAesXcbc() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final byte[] authKey = getKeyBytes(AES_XCBC_KEY_LEN);
+        final IpSecAlgorithm ipsecAuthAlgo =
+                new IpSecAlgorithm(IpSecAlgorithm.AUTH_AES_XCBC, authKey, AES_XCBC_ICV_LEN * 8);
+        final EspAuth espAuth = new EspAuth(AES_XCBC, authKey, AES_XCBC_ICV_LEN);
+
+        runWithShellPermissionIdentity(new TestNetworkRunnable(new CheckCryptoImplTest(
+                null /* ipsecEncryptAlgo */, ipsecAuthAlgo, null /* ipsecAeadAlgo */,
+                EspCipherNull.getInstance(), espAuth)));
+    }
+
+    @Test
+    public void testAesCmac() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final byte[] authKey = getKeyBytes(AES_CMAC_KEY_LEN);
+        final IpSecAlgorithm ipsecAuthAlgo =
+                new IpSecAlgorithm(IpSecAlgorithm.AUTH_AES_CMAC, authKey, AES_CMAC_ICV_LEN * 8);
+        final EspAuth espAuth = new EspAuth(AES_CMAC, authKey, AES_CMAC_ICV_LEN);
+
+        runWithShellPermissionIdentity(new TestNetworkRunnable(new CheckCryptoImplTest(
+                null /* ipsecEncryptAlgo */, ipsecAuthAlgo, null /* ipsecAeadAlgo */,
+                EspCipherNull.getInstance(), espAuth)));
+    }
+
+    @Test
+    public void testChaCha20Poly1305() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final byte[] cryptKey = getKeyBytes(CHACHA20_POLY1305_KEY_LEN);
+        final IpSecAlgorithm ipsecAeadAlgo =
+                new IpSecAlgorithm(
+                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305,
+                        cryptKey,
+                        CHACHA20_POLY1305_ICV_LEN * 8);
+        final EspAeadCipher espAead =
+                new EspAeadCipher(
+                        CHACHA20_POLY1305,
+                        CHACHA20_POLY1305_BLK_SIZE,
+                        cryptKey,
+                        CHACHA20_POLY1305_IV_LEN,
+                        CHACHA20_POLY1305_ICV_LEN,
+                        CHACHA20_POLY1305_SALT_LEN);
+
+        runWithShellPermissionIdentity(
+                new TestNetworkRunnable(
+                        new CheckCryptoImplTest(
+                                null /* ipsecEncryptAlgo */,
+                                null /* ipsecAuthAlgo */,
+                                ipsecAeadAlgo,
+                                espAead,
+                                EspAuthNull.getInstance())));
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
index 10e43e7..c54ee91 100644
--- a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
@@ -16,6 +16,14 @@
 
 package android.net.cts;
 
+import static android.net.IpSecAlgorithm.AUTH_CRYPT_AES_GCM;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_MD5;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA1;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA256;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA384;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA512;
+import static android.net.IpSecAlgorithm.CRYPT_AES_CBC;
+
 import static org.junit.Assert.assertArrayEquals;
 
 import android.content.Context;
@@ -31,6 +39,12 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.build.SdkLevel;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -42,12 +56,10 @@
 import java.net.Socket;
 import java.net.SocketException;
 import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
 @RunWith(AndroidJUnit4.class)
 public class IpSecBaseTest {
 
@@ -71,6 +83,18 @@
         0x20, 0x21, 0x22, 0x23
     };
 
+    private static final Set<String> MANDATORY_IPSEC_ALGOS_SINCE_P = new HashSet<>();
+
+    static {
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(CRYPT_AES_CBC);
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(AUTH_HMAC_MD5);
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(AUTH_HMAC_SHA1);
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(AUTH_HMAC_SHA256);
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(AUTH_HMAC_SHA384);
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(AUTH_HMAC_SHA512);
+        MANDATORY_IPSEC_ALGOS_SINCE_P.add(AUTH_CRYPT_AES_GCM);
+    }
+
     protected static final byte[] AUTH_KEY = getKey(256);
     protected static final byte[] CRYPT_KEY = getKey(256);
 
@@ -89,8 +113,24 @@
                                 .getSystemService(Context.CONNECTIVITY_SERVICE);
     }
 
+    /** Checks if an IPsec algorithm is enabled on the device */
+    protected static boolean hasIpSecAlgorithm(String algorithm) {
+        if (SdkLevel.isAtLeastS()) {
+            return IpSecAlgorithm.getSupportedAlgorithms().contains(algorithm);
+        } else {
+            return MANDATORY_IPSEC_ALGOS_SINCE_P.contains(algorithm);
+        }
+    }
+
+    protected static byte[] getKeyBytes(int byteLength) {
+        return Arrays.copyOf(KEY_DATA, byteLength);
+    }
+
     protected static byte[] getKey(int bitLength) {
-        return Arrays.copyOf(KEY_DATA, bitLength / 8);
+        if (bitLength % 8 != 0) {
+            throw new IllegalArgumentException("Invalid key length in bits" + bitLength);
+        }
+        return getKeyBytes(bitLength / 8);
     }
 
     protected static int getDomain(InetAddress address) {
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index 355b496..5c95aa3 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -16,10 +16,33 @@
 
 package android.net.cts;
 
+import static android.net.IpSecAlgorithm.AUTH_AES_CMAC;
+import static android.net.IpSecAlgorithm.AUTH_AES_XCBC;
+import static android.net.IpSecAlgorithm.AUTH_CRYPT_AES_GCM;
+import static android.net.IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_MD5;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA1;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA256;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA384;
+import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA512;
+import static android.net.IpSecAlgorithm.CRYPT_AES_CBC;
+import static android.net.IpSecAlgorithm.CRYPT_AES_CTR;
 import static android.net.cts.PacketUtils.AES_CBC_BLK_SIZE;
 import static android.net.cts.PacketUtils.AES_CBC_IV_LEN;
+import static android.net.cts.PacketUtils.AES_CMAC_ICV_LEN;
+import static android.net.cts.PacketUtils.AES_CMAC_KEY_LEN;
+import static android.net.cts.PacketUtils.AES_CTR_BLK_SIZE;
+import static android.net.cts.PacketUtils.AES_CTR_IV_LEN;
+import static android.net.cts.PacketUtils.AES_CTR_KEY_LEN_20;
 import static android.net.cts.PacketUtils.AES_GCM_BLK_SIZE;
 import static android.net.cts.PacketUtils.AES_GCM_IV_LEN;
+import static android.net.cts.PacketUtils.AES_XCBC_ICV_LEN;
+import static android.net.cts.PacketUtils.AES_XCBC_KEY_LEN;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_BLK_SIZE;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_ICV_LEN;
+import static android.net.cts.PacketUtils.CHACHA20_POLY1305_IV_LEN;
+import static android.net.cts.PacketUtils.HMAC_SHA512_ICV_LEN;
+import static android.net.cts.PacketUtils.HMAC_SHA512_KEY_LEN;
 import static android.net.cts.PacketUtils.IP4_HDRLEN;
 import static android.net.cts.PacketUtils.IP6_HDRLEN;
 import static android.net.cts.PacketUtils.TCP_HDRLEN_WITH_TIMESTAMP_OPT;
@@ -27,15 +50,20 @@
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
 
+import static com.android.compatibility.common.util.PropertyUtil.getFirstApiLevel;
+import static com.android.compatibility.common.util.PropertyUtil.getVendorApiLevel;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.net.IpSecAlgorithm;
 import android.net.IpSecManager;
 import android.net.IpSecTransform;
 import android.net.TrafficStats;
+import android.os.Build;
 import android.platform.test.annotations.AppModeFull;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -44,6 +72,13 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.DatagramPacket;
@@ -51,14 +86,15 @@
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.util.Arrays;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
 @AppModeFull(reason = "Socket cannot bind in instant app mode")
 public class IpSecManagerTest extends IpSecBaseTest {
+    @Rule public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
 
     private static final String TAG = IpSecManagerTest.class.getSimpleName();
 
@@ -416,8 +452,12 @@
         switch (cryptOrAead.getName()) {
             case IpSecAlgorithm.CRYPT_AES_CBC:
                 return AES_CBC_IV_LEN;
+            case IpSecAlgorithm.CRYPT_AES_CTR:
+                return AES_CTR_IV_LEN;
             case IpSecAlgorithm.AUTH_CRYPT_AES_GCM:
                 return AES_GCM_IV_LEN;
+            case IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305:
+                return CHACHA20_POLY1305_IV_LEN;
             default:
                 throw new IllegalArgumentException(
                         "IV length unknown for algorithm" + cryptOrAead.getName());
@@ -432,8 +472,12 @@
         switch (cryptOrAead.getName()) {
             case IpSecAlgorithm.CRYPT_AES_CBC:
                 return AES_CBC_BLK_SIZE;
+            case IpSecAlgorithm.CRYPT_AES_CTR:
+                return AES_CTR_BLK_SIZE;
             case IpSecAlgorithm.AUTH_CRYPT_AES_GCM:
                 return AES_GCM_BLK_SIZE;
+            case IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305:
+                return CHACHA20_POLY1305_BLK_SIZE;
             default:
                 throw new IllegalArgumentException(
                         "Blk size unknown for algorithm" + cryptOrAead.getName());
@@ -515,7 +559,6 @@
             int blkSize,
             int truncLenBits)
             throws Exception {
-
         int innerPacketSize = TEST_DATA.length + transportHdrLen + ipHdrLen;
         int outerPacketSize =
                 PacketUtils.calculateEspPacketSize(
@@ -662,6 +705,41 @@
     //     checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, true, 1000);
     // }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testGetSupportedAlgorithms() throws Exception {
+        final Map<String, Integer> algoToRequiredMinSdk = new HashMap<>();
+        algoToRequiredMinSdk.put(CRYPT_AES_CBC, Build.VERSION_CODES.P);
+        algoToRequiredMinSdk.put(AUTH_HMAC_MD5, Build.VERSION_CODES.P);
+        algoToRequiredMinSdk.put(AUTH_HMAC_SHA1, Build.VERSION_CODES.P);
+        algoToRequiredMinSdk.put(AUTH_HMAC_SHA256, Build.VERSION_CODES.P);
+        algoToRequiredMinSdk.put(AUTH_HMAC_SHA384, Build.VERSION_CODES.P);
+        algoToRequiredMinSdk.put(AUTH_HMAC_SHA512, Build.VERSION_CODES.P);
+        algoToRequiredMinSdk.put(AUTH_CRYPT_AES_GCM, Build.VERSION_CODES.P);
+
+        // TODO: b/170424293 Use Build.VERSION_CODES.S when is finalized
+        algoToRequiredMinSdk.put(CRYPT_AES_CTR, Build.VERSION_CODES.R + 1);
+        algoToRequiredMinSdk.put(AUTH_AES_CMAC, Build.VERSION_CODES.R + 1);
+        algoToRequiredMinSdk.put(AUTH_AES_XCBC, Build.VERSION_CODES.R + 1);
+        algoToRequiredMinSdk.put(AUTH_CRYPT_CHACHA20_POLY1305, Build.VERSION_CODES.R + 1);
+
+        final Set<String> supportedAlgos = IpSecAlgorithm.getSupportedAlgorithms();
+
+        // Verify all supported algorithms are valid
+        for (String algo : supportedAlgos) {
+            assertTrue("Found invalid algo " + algo, algoToRequiredMinSdk.keySet().contains(algo));
+        }
+
+        // Verify all mandatory algorithms are supported
+        for (Entry<String, Integer> entry : algoToRequiredMinSdk.entrySet()) {
+            if (Math.min(getFirstApiLevel(), getVendorApiLevel()) >= entry.getValue()) {
+                assertTrue(
+                        "Fail to support " + entry.getKey(),
+                        supportedAlgos.contains(entry.getKey()));
+            }
+        }
+    }
+
     @Test
     public void testInterfaceCountersUdp4() throws Exception {
         IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
@@ -843,6 +921,149 @@
         checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
     }
 
+    private static IpSecAlgorithm buildCryptAesCtr() throws Exception {
+        return new IpSecAlgorithm(CRYPT_AES_CTR, getKeyBytes(AES_CTR_KEY_LEN_20));
+    }
+
+    private static IpSecAlgorithm buildAuthHmacSha512() throws Exception {
+        return new IpSecAlgorithm(
+                AUTH_HMAC_SHA512, getKeyBytes(HMAC_SHA512_KEY_LEN), HMAC_SHA512_ICV_LEN * 8);
+    }
+
+    @Test
+    public void testAesCtrHmacSha512Tcp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        final IpSecAlgorithm crypt = buildCryptAesCtr();
+        final IpSecAlgorithm auth = buildAuthHmacSha512();
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCtrHmacSha512Tcp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        final IpSecAlgorithm crypt = buildCryptAesCtr();
+        final IpSecAlgorithm auth = buildAuthHmacSha512();
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCtrHmacSha512Udp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        final IpSecAlgorithm crypt = buildCryptAesCtr();
+        final IpSecAlgorithm auth = buildAuthHmacSha512();
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCtrHmacSha512Udp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        final IpSecAlgorithm crypt = buildCryptAesCtr();
+        final IpSecAlgorithm auth = buildAuthHmacSha512();
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    private static IpSecAlgorithm buildCryptAesCbc() throws Exception {
+        return new IpSecAlgorithm(CRYPT_AES_CBC, CRYPT_KEY);
+    }
+
+    private static IpSecAlgorithm buildAuthAesXcbc() throws Exception {
+        return new IpSecAlgorithm(
+                AUTH_AES_XCBC, getKeyBytes(AES_XCBC_KEY_LEN), AES_XCBC_ICV_LEN * 8);
+    }
+
+    private static IpSecAlgorithm buildAuthAesCmac() throws Exception {
+        return new IpSecAlgorithm(
+                AUTH_AES_CMAC, getKeyBytes(AES_CMAC_KEY_LEN), AES_CMAC_ICV_LEN * 8);
+    }
+
+    @Test
+    public void testAesCbcAesXCbcTcp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesXcbc();
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesXCbcTcp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesXcbc();
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesXCbcUdp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesXcbc();
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesXCbcUdp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesXcbc();
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesCmacTcp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesCmac();
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesCmacTcp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesCmac();
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesCmacUdp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesCmac();
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesCmacUdp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final IpSecAlgorithm crypt = buildCryptAesCbc();
+        final IpSecAlgorithm auth = buildAuthAesCmac();
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, crypt, auth, null, false, 1, true);
+    }
+
     @Test
     public void testAesGcm64Tcp4() throws Exception {
         IpSecAlgorithm authCrypt =
@@ -939,6 +1160,47 @@
         checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
     }
 
+    private static IpSecAlgorithm buildAuthCryptChaCha20Poly1305() throws Exception {
+        return new IpSecAlgorithm(
+                AUTH_CRYPT_CHACHA20_POLY1305, AEAD_KEY, CHACHA20_POLY1305_ICV_LEN * 8);
+    }
+
+    @Test
+    public void testChaCha20Poly1305Tcp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final IpSecAlgorithm authCrypt = buildAuthCryptChaCha20Poly1305();
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+    }
+
+    @Test
+    public void testChaCha20Poly1305Tcp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final IpSecAlgorithm authCrypt = buildAuthCryptChaCha20Poly1305();
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+        checkTransform(IPPROTO_TCP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+    }
+
+    @Test
+    public void testChaCha20Poly1305Udp4() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final IpSecAlgorithm authCrypt = buildAuthCryptChaCha20Poly1305();
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, false, 1, true);
+    }
+
+    @Test
+    public void testChaCha20Poly1305Udp6() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final IpSecAlgorithm authCrypt = buildAuthCryptChaCha20Poly1305();
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, false);
+        checkTransform(IPPROTO_UDP, IPV6_LOOPBACK, null, null, authCrypt, false, 1, true);
+    }
+
     @Test
     public void testAesCbcHmacMd5Tcp4UdpEncap() throws Exception {
         IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
@@ -1020,6 +1282,66 @@
     }
 
     @Test
+    public void testAesCtrHmacSha512Tcp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        final IpSecAlgorithm crypt = buildCryptAesCtr();
+        final IpSecAlgorithm auth = buildAuthHmacSha512();
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+    }
+
+    @Test
+    public void testAesCtrHmacSha512Udp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(CRYPT_AES_CTR));
+
+        final IpSecAlgorithm crypt = buildCryptAesCtr();
+        final IpSecAlgorithm auth = buildAuthHmacSha512();
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesXCbcTcp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final IpSecAlgorithm crypt = new IpSecAlgorithm(CRYPT_AES_CBC, CRYPT_KEY);
+        final IpSecAlgorithm auth = new IpSecAlgorithm(AUTH_AES_XCBC, getKey(128), 96);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesXCbcUdp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_XCBC));
+
+        final IpSecAlgorithm crypt = new IpSecAlgorithm(CRYPT_AES_CBC, CRYPT_KEY);
+        final IpSecAlgorithm auth = new IpSecAlgorithm(AUTH_AES_XCBC, getKey(128), 96);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesCmacTcp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final IpSecAlgorithm crypt = new IpSecAlgorithm(CRYPT_AES_CBC, CRYPT_KEY);
+        final IpSecAlgorithm auth = new IpSecAlgorithm(AUTH_AES_CMAC, getKey(128), 96);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+    }
+
+    @Test
+    public void testAesCbcAesCmacUdp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_AES_CMAC));
+
+        final IpSecAlgorithm crypt = new IpSecAlgorithm(CRYPT_AES_CBC, CRYPT_KEY);
+        final IpSecAlgorithm auth = new IpSecAlgorithm(AUTH_AES_CMAC, getKey(128), 96);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, auth, null, true, 1, true);
+    }
+
+    @Test
     public void testAesGcm64Tcp4UdpEncap() throws Exception {
         IpSecAlgorithm authCrypt =
                 new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 64);
@@ -1068,6 +1390,24 @@
     }
 
     @Test
+    public void testChaCha20Poly1305Tcp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final IpSecAlgorithm authCrypt = buildAuthCryptChaCha20Poly1305();
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+        checkTransform(IPPROTO_TCP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+    }
+
+    @Test
+    public void testChaCha20Poly1305Udp4UdpEncap() throws Exception {
+        assumeTrue(hasIpSecAlgorithm(AUTH_CRYPT_CHACHA20_POLY1305));
+
+        final IpSecAlgorithm authCrypt = buildAuthCryptChaCha20Poly1305();
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, false);
+        checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, null, null, authCrypt, true, 1, true);
+    }
+
+    @Test
     public void testCryptUdp4() throws Exception {
         IpSecAlgorithm crypt = new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
         checkTransform(IPPROTO_UDP, IPV4_LOOPBACK, crypt, null, null, false, 1, false);
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
index ae38faa..a9a3380 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTunnelTest.java
@@ -43,6 +43,7 @@
 import android.net.ConnectivityManager;
 import android.net.IpSecAlgorithm;
 import android.net.IpSecManager;
+import android.net.IpSecManager.IpSecTunnelInterface;
 import android.net.IpSecTransform;
 import android.net.LinkAddress;
 import android.net.Network;
@@ -50,25 +51,33 @@
 import android.net.TestNetworkManager;
 import android.net.cts.PacketUtils.Payload;
 import android.net.cts.util.CtsNetUtils;
+import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.platform.test.annotations.AppModeFull;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
 import org.junit.AfterClass;
 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.NetworkInterface;
+
 @RunWith(AndroidJUnit4.class)
 @AppModeFull(reason = "MANAGE_TEST_NETWORKS permission can't be granted to instant apps")
 public class IpSecManagerTunnelTest extends IpSecBaseTest {
+    @Rule public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
     private static final String TAG = IpSecManagerTunnelTest.class.getSimpleName();
 
     private static final InetAddress LOCAL_OUTER_4 = InetAddress.parseNumericAddress("192.0.2.1");
@@ -78,6 +87,15 @@
     private static final InetAddress REMOTE_OUTER_6 =
             InetAddress.parseNumericAddress("2001:db8:1::2");
 
+    private static final InetAddress LOCAL_OUTER_4_NEW =
+            InetAddress.parseNumericAddress("192.0.2.101");
+    private static final InetAddress REMOTE_OUTER_4_NEW =
+            InetAddress.parseNumericAddress("192.0.2.102");
+    private static final InetAddress LOCAL_OUTER_6_NEW =
+            InetAddress.parseNumericAddress("2001:db8:1::101");
+    private static final InetAddress REMOTE_OUTER_6_NEW =
+            InetAddress.parseNumericAddress("2001:db8:1::102");
+
     private static final InetAddress LOCAL_INNER_4 =
             InetAddress.parseNumericAddress("198.51.100.1");
     private static final InetAddress REMOTE_INNER_4 =
@@ -95,10 +113,9 @@
     // Static state to reduce setup/teardown
     private static ConnectivityManager sCM;
     private static TestNetworkManager sTNM;
-    private static ParcelFileDescriptor sTunFd;
-    private static TestNetworkCallback sTunNetworkCallback;
-    private static Network sTunNetwork;
-    private static TunUtils sTunUtils;
+
+    private static TunNetworkWrapper sTunWrapper;
+    private static TunNetworkWrapper sTunWrapperNew;
 
     private static Context sContext = InstrumentationRegistry.getContext();
     private static final CtsNetUtils mCtsNetUtils = new CtsNetUtils(sContext);
@@ -116,19 +133,8 @@
         // right appop permissions.
         mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, true);
 
-        TestNetworkInterface testIface =
-                sTNM.createTunInterface(
-                        new LinkAddress[] {
-                            new LinkAddress(LOCAL_OUTER_4, IP4_PREFIX_LEN),
-                            new LinkAddress(LOCAL_OUTER_6, IP6_PREFIX_LEN)
-                        });
-
-        sTunFd = testIface.getFileDescriptor();
-        sTunNetworkCallback = mCtsNetUtils.setupAndGetTestNetwork(testIface.getInterfaceName());
-        sTunNetworkCallback.waitForAvailable();
-        sTunNetwork = sTunNetworkCallback.currentNetwork;
-
-        sTunUtils = new TunUtils(sTunFd);
+        sTunWrapper = new TunNetworkWrapper(LOCAL_OUTER_4, LOCAL_OUTER_6);
+        sTunWrapperNew = new TunNetworkWrapper(LOCAL_OUTER_4_NEW, LOCAL_OUTER_6_NEW);
     }
 
     @Before
@@ -139,24 +145,76 @@
         // Set to true before every run; some tests flip this.
         mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, true);
 
-        // Clear sTunUtils state
-        sTunUtils.reset();
+        // Clear TunUtils state
+        sTunWrapper.utils.reset();
+        sTunWrapperNew.utils.reset();
+    }
+
+    private static void tearDownTunWrapperIfNotNull(TunNetworkWrapper tunWrapper) throws Exception {
+        if (tunWrapper != null) {
+            tunWrapper.tearDown();
+        }
     }
 
     @AfterClass
     public static void tearDownAfterClass() throws Exception {
         mCtsNetUtils.setAppopPrivileged(OP_MANAGE_IPSEC_TUNNELS, false);
 
-        sCM.unregisterNetworkCallback(sTunNetworkCallback);
-
-        sTNM.teardownTestNetwork(sTunNetwork);
-        sTunFd.close();
+        tearDownTunWrapperIfNotNull(sTunWrapper);
+        tearDownTunWrapperIfNotNull(sTunWrapperNew);
 
         InstrumentationRegistry.getInstrumentation()
                 .getUiAutomation()
                 .dropShellPermissionIdentity();
     }
 
+    private static class TunNetworkWrapper {
+        public final ParcelFileDescriptor fd;
+        public final TestNetworkCallback networkCallback;
+        public final Network network;
+        public final TunUtils utils;
+
+        TunNetworkWrapper(InetAddress... addresses) throws Exception {
+            final LinkAddress[] linkAddresses = new LinkAddress[addresses.length];
+            for (int i = 0; i < linkAddresses.length; i++) {
+                InetAddress addr = addresses[i];
+                if (addr instanceof Inet4Address) {
+                    linkAddresses[i] = new LinkAddress(addr, IP4_PREFIX_LEN);
+                } else {
+                    linkAddresses[i] = new LinkAddress(addr, IP6_PREFIX_LEN);
+                }
+            }
+
+            try {
+                final TestNetworkInterface testIface = sTNM.createTunInterface(linkAddresses);
+
+                fd = testIface.getFileDescriptor();
+                networkCallback = mCtsNetUtils.setupAndGetTestNetwork(testIface.getInterfaceName());
+                networkCallback.waitForAvailable();
+                network = networkCallback.currentNetwork;
+            } catch (Exception e) {
+                tearDown();
+                throw e;
+            }
+
+            utils = new TunUtils(fd);
+        }
+
+        public void tearDown() throws Exception {
+            if (networkCallback != null) {
+                sCM.unregisterNetworkCallback(networkCallback);
+            }
+
+            if (network != null) {
+                sTNM.teardownTestNetwork(network);
+            }
+
+            if (fd != null) {
+                fd.close();
+            }
+        }
+    }
+
     @Test
     public void testSecurityExceptionCreateTunnelInterfaceWithoutAppop() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
@@ -166,7 +224,7 @@
 
         // Security exceptions are thrown regardless of IPv4/IPv6. Just test one
         try {
-            mISM.createIpSecTunnelInterface(LOCAL_INNER_6, REMOTE_INNER_6, sTunNetwork);
+            mISM.createIpSecTunnelInterface(LOCAL_INNER_6, REMOTE_INNER_6, sTunWrapper.network);
             fail("Did not throw SecurityException for Tunnel creation without appop");
         } catch (SecurityException expected) {
         }
@@ -196,11 +254,16 @@
          * Runs the test code, and returns the inner socket port, if any.
          *
          * @param ipsecNetwork The IPsec Interface based Network for binding sockets on
+         * @param tunnelIface The IPsec tunnel interface that will be tested
+         * @param underlyingTunUtils The utility of the IPsec tunnel interface's underlying TUN
+         *     network
          * @return the integer port of the inner socket if outbound, or 0 if inbound
          *     IpSecTunnelTestRunnable
          * @throws Exception if any part of the test failed.
          */
-        public abstract int run(Network ipsecNetwork) throws Exception;
+        public abstract int run(
+                Network ipsecNetwork, IpSecTunnelInterface tunnelIface, TunUtils underlyingTunUtils)
+                throws Exception;
     }
 
     private int getPacketSize(
@@ -265,7 +328,9 @@
                 int expectedPacketSize) {
             return new IpSecTunnelTestRunnable() {
                 @Override
-                public int run(Network ipsecNetwork) throws Exception {
+                public int run(
+                        Network ipsecNetwork, IpSecTunnelInterface tunnelIface, TunUtils tunUtils)
+                        throws Exception {
                     // Build a socket and send traffic
                     JavaUdpSocket socket = new JavaUdpSocket(localInner);
                     ipsecNetwork.bindSocket(socket.mSocket);
@@ -284,7 +349,7 @@
                     // Verify that an encrypted packet is sent. As of right now, checking encrypted
                     // body is not possible, due to the test not knowing some of the fields of the
                     // inner IP header (flow label, flags, etc)
-                    sTunUtils.awaitEspPacketNoPlaintext(
+                    tunUtils.awaitEspPacketNoPlaintext(
                             spi, TEST_DATA, encapPort != 0, expectedPacketSize);
 
                     socket.close();
@@ -312,7 +377,9 @@
                 throws Exception {
             return new IpSecTunnelTestRunnable() {
                 @Override
-                public int run(Network ipsecNetwork) throws Exception {
+                public int run(
+                        Network ipsecNetwork, IpSecTunnelInterface tunnelIface, TunUtils tunUtils)
+                        throws Exception {
                     // Build a socket and receive traffic
                     JavaUdpSocket socket = new JavaUdpSocket(localInner, innerSocketPort);
                     ipsecNetwork.bindSocket(socket.mSocket);
@@ -325,7 +392,7 @@
                                 socket.mSocket, IpSecManager.DIRECTION_OUT, inTransportTransform);
                     }
 
-                    sTunUtils.reflectPackets();
+                    tunUtils.reflectPackets();
 
                     // Receive packet from socket, and validate that the payload is correct
                     receiveAndValidatePacket(socket);
@@ -355,7 +422,9 @@
                 throws Exception {
             return new IpSecTunnelTestRunnable() {
                 @Override
-                public int run(Network ipsecNetwork) throws Exception {
+                public int run(
+                        Network ipsecNetwork, IpSecTunnelInterface tunnelIface, TunUtils tunUtils)
+                        throws Exception {
                     // Build a socket and receive traffic
                     JavaUdpSocket socket = new JavaUdpSocket(localInner);
                     ipsecNetwork.bindSocket(socket.mSocket);
@@ -391,7 +460,7 @@
                                         socket.getPort(),
                                         encapPort);
                     }
-                    sTunUtils.injectPacket(pkt);
+                    tunUtils.injectPacket(pkt);
 
                     // Receive packet from socket, and validate
                     receiveAndValidatePacket(socket);
@@ -404,6 +473,161 @@
         }
     }
 
+    private class MigrateIpSecTunnelTestRunnableFactory implements IpSecTunnelTestRunnableFactory {
+        private final IpSecTunnelTestRunnableFactory mTestRunnableFactory;
+
+        MigrateIpSecTunnelTestRunnableFactory(boolean isOutputTest) {
+            if (isOutputTest) {
+                mTestRunnableFactory = new OutputIpSecTunnelTestRunnableFactory();
+            } else {
+                mTestRunnableFactory = new InputPacketGeneratorIpSecTunnelTestRunnableFactory();
+            }
+        }
+
+        @Override
+        public IpSecTunnelTestRunnable getIpSecTunnelTestRunnable(
+                boolean transportInTunnelMode,
+                int spi,
+                InetAddress localInner,
+                InetAddress remoteInner,
+                InetAddress localOuter,
+                InetAddress remoteOuter,
+                IpSecTransform inTransportTransform,
+                IpSecTransform outTransportTransform,
+                int encapPort,
+                int unusedInnerSocketPort,
+                int expectedPacketSize) {
+            return new IpSecTunnelTestRunnable() {
+                @Override
+                public int run(
+                        Network ipsecNetwork, IpSecTunnelInterface tunnelIface, TunUtils tunUtils)
+                        throws Exception {
+                    mTestRunnableFactory
+                            .getIpSecTunnelTestRunnable(
+                                    transportInTunnelMode,
+                                    spi,
+                                    localInner,
+                                    remoteInner,
+                                    localOuter,
+                                    remoteOuter,
+                                    inTransportTransform,
+                                    outTransportTransform,
+                                    encapPort,
+                                    unusedInnerSocketPort,
+                                    expectedPacketSize)
+                            .run(ipsecNetwork, tunnelIface, sTunWrapper.utils);
+
+                    tunnelIface.setUnderlyingNetwork(sTunWrapperNew.network);
+
+                    // Verify migrating to IPv4 and IPv6 addresses. It ensures that not only
+                    // can IPsec tunnel migrate across interfaces, IPsec tunnel can also migrate to
+                    // a different address on the same interface.
+                    checkMigratedTunnel(
+                            localInner,
+                            remoteInner,
+                            LOCAL_OUTER_4_NEW,
+                            REMOTE_OUTER_4_NEW,
+                            encapPort != 0,
+                            transportInTunnelMode,
+                            sTunWrapperNew.utils,
+                            tunnelIface,
+                            ipsecNetwork);
+                    checkMigratedTunnel(
+                            localInner,
+                            remoteInner,
+                            LOCAL_OUTER_6_NEW,
+                            REMOTE_OUTER_6_NEW,
+                            false, // IPv6 does not support UDP encapsulation
+                            transportInTunnelMode,
+                            sTunWrapperNew.utils,
+                            tunnelIface,
+                            ipsecNetwork);
+
+                    return 0;
+                }
+            };
+        }
+
+        private void checkMigratedTunnel(
+                InetAddress localInner,
+                InetAddress remoteInner,
+                InetAddress localOuter,
+                InetAddress remoteOuter,
+                boolean useEncap,
+                boolean transportInTunnelMode,
+                TunUtils tunUtils,
+                IpSecTunnelInterface tunnelIface,
+                Network ipsecNetwork)
+                throws Exception {
+
+            // Preselect both SPI and encap port, to be used for both inbound and outbound tunnels.
+            // Re-uses the same SPI to ensure that even in cases of symmetric SPIs shared across
+            // tunnel and transport mode, packets are encrypted/decrypted properly based on the
+            // src/dst.
+            int spi = getRandomSpi(localOuter, remoteOuter);
+
+            int innerFamily = localInner instanceof Inet4Address ? AF_INET : AF_INET6;
+            int outerFamily = localOuter instanceof Inet4Address ? AF_INET : AF_INET6;
+            int expectedPacketSize =
+                    getPacketSize(innerFamily, outerFamily, useEncap, transportInTunnelMode);
+
+            // Build transport mode transforms and encapsulation socket for verifying
+            // transport-in-tunnel case and encapsulation case.
+            try (IpSecManager.SecurityParameterIndex inTransportSpi =
+                            mISM.allocateSecurityParameterIndex(localInner, spi);
+                    IpSecManager.SecurityParameterIndex outTransportSpi =
+                            mISM.allocateSecurityParameterIndex(remoteInner, spi);
+                    IpSecTransform inTransportTransform =
+                            buildIpSecTransform(sContext, inTransportSpi, null, remoteInner);
+                    IpSecTransform outTransportTransform =
+                            buildIpSecTransform(sContext, outTransportSpi, null, localInner);
+                    UdpEncapsulationSocket encapSocket = mISM.openUdpEncapsulationSocket()) {
+
+                // Configure tunnel mode Transform parameters
+                IpSecTransform.Builder transformBuilder = new IpSecTransform.Builder(sContext);
+                transformBuilder.setEncryption(
+                        new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY));
+                transformBuilder.setAuthentication(
+                        new IpSecAlgorithm(
+                                IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4));
+
+                if (useEncap) {
+                    transformBuilder.setIpv4Encapsulation(encapSocket, encapSocket.getPort());
+                }
+
+                // Apply transform and check that traffic is properly encrypted
+                try (IpSecManager.SecurityParameterIndex inSpi =
+                                mISM.allocateSecurityParameterIndex(localOuter, spi);
+                        IpSecManager.SecurityParameterIndex outSpi =
+                                mISM.allocateSecurityParameterIndex(remoteOuter, spi);
+                        IpSecTransform inTransform =
+                                transformBuilder.buildTunnelModeTransform(remoteOuter, inSpi);
+                        IpSecTransform outTransform =
+                                transformBuilder.buildTunnelModeTransform(localOuter, outSpi)) {
+                    mISM.applyTunnelModeTransform(
+                            tunnelIface, IpSecManager.DIRECTION_IN, inTransform);
+                    mISM.applyTunnelModeTransform(
+                            tunnelIface, IpSecManager.DIRECTION_OUT, outTransform);
+
+                    mTestRunnableFactory
+                            .getIpSecTunnelTestRunnable(
+                                    transportInTunnelMode,
+                                    spi,
+                                    localInner,
+                                    remoteInner,
+                                    localOuter,
+                                    remoteOuter,
+                                    inTransportTransform,
+                                    outTransportTransform,
+                                    useEncap ? encapSocket.getPort() : 0,
+                                    0,
+                                    expectedPacketSize)
+                            .run(ipsecNetwork, tunnelIface, tunUtils);
+                }
+            }
+        }
+    }
+
     private void checkTunnelOutput(
             int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode)
             throws Exception {
@@ -426,6 +650,28 @@
                 new InputPacketGeneratorIpSecTunnelTestRunnableFactory());
     }
 
+    private void checkMigrateTunnelOutput(
+            int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode)
+            throws Exception {
+        checkTunnel(
+                innerFamily,
+                outerFamily,
+                useEncap,
+                transportInTunnelMode,
+                new MigrateIpSecTunnelTestRunnableFactory(true));
+    }
+
+    private void checkMigrateTunnelInput(
+            int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode)
+            throws Exception {
+        checkTunnel(
+                innerFamily,
+                outerFamily,
+                useEncap,
+                transportInTunnelMode,
+                new MigrateIpSecTunnelTestRunnableFactory(false));
+    }
+
     /**
      * Validates that the kernel can talk to itself.
      *
@@ -579,7 +825,8 @@
                 IpSecManager.SecurityParameterIndex outSpi =
                         mISM.allocateSecurityParameterIndex(remoteOuter, spi);
                 IpSecManager.IpSecTunnelInterface tunnelIface =
-                        mISM.createIpSecTunnelInterface(localOuter, remoteOuter, sTunNetwork)) {
+                        mISM.createIpSecTunnelInterface(
+                                localOuter, remoteOuter, sTunWrapper.network)) {
             // Build the test network
             tunnelIface.addAddress(localInner, innerPrefixLen);
             testNetworkCb = mCtsNetUtils.setupAndGetTestNetwork(tunnelIface.getInterfaceName());
@@ -615,7 +862,7 @@
                 mISM.applyTunnelModeTransform(
                         tunnelIface, IpSecManager.DIRECTION_OUT, outTransform);
 
-                innerSocketPort = test.run(testNetwork);
+                innerSocketPort = test.run(testNetwork, tunnelIface, sTunWrapper.utils);
             }
 
             // Teardown the test network
@@ -739,6 +986,14 @@
         return maybeEncapPacket(srcOuter, dstOuter, encapPort, espPayload).getPacketBytes();
     }
 
+    private void doTestMigrateTunnel(
+            int innerFamily, int outerFamily, boolean useEncap, boolean transportInTunnelMode)
+            throws Exception {
+        assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+        checkTunnelOutput(innerFamily, outerFamily, useEncap, transportInTunnelMode);
+        checkTunnelInput(innerFamily, outerFamily, useEncap, transportInTunnelMode);
+    }
+
     // Transport-in-Tunnel mode tests
     @Test
     public void testTransportInTunnelModeV4InV4() throws Exception {
@@ -747,6 +1002,12 @@
         checkTunnelInput(AF_INET, AF_INET, false, true);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testMigrateTransportInTunnelModeV4InV4() throws Exception {
+        doTestMigrateTunnel(AF_INET, AF_INET, false, true);
+    }
+
     @Test
     public void testTransportInTunnelModeV4InV4Reflected() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
@@ -760,6 +1021,12 @@
         checkTunnelInput(AF_INET, AF_INET, true, true);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testMigrateTransportInTunnelModeV4InV4UdpEncap() throws Exception {
+        doTestMigrateTunnel(AF_INET, AF_INET, true, true);
+    }
+
     @Test
     public void testTransportInTunnelModeV4InV4UdpEncapReflected() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
@@ -773,6 +1040,12 @@
         checkTunnelInput(AF_INET, AF_INET6, false, true);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testMigrateTransportInTunnelModeV4InV6() throws Exception {
+        doTestMigrateTunnel(AF_INET, AF_INET6, false, true);
+    }
+
     @Test
     public void testTransportInTunnelModeV4InV6Reflected() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
@@ -786,6 +1059,12 @@
         checkTunnelInput(AF_INET6, AF_INET, false, true);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testMigrateTransportInTunnelModeV6InV4() throws Exception {
+        doTestMigrateTunnel(AF_INET6, AF_INET, false, true);
+    }
+
     @Test
     public void testTransportInTunnelModeV6InV4Reflected() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
@@ -799,6 +1078,12 @@
         checkTunnelInput(AF_INET6, AF_INET, true, true);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testMigrateTransportInTunnelModeV6InV4UdpEncap() throws Exception {
+        doTestMigrateTunnel(AF_INET6, AF_INET, true, true);
+    }
+
     @Test
     public void testTransportInTunnelModeV6InV4UdpEncapReflected() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
@@ -812,6 +1097,12 @@
         checkTunnelInput(AF_INET, AF_INET6, false, true);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testMigrateTransportInTunnelModeV6InV6() throws Exception {
+        doTestMigrateTunnel(AF_INET, AF_INET6, false, true);
+    }
+
     @Test
     public void testTransportInTunnelModeV6InV6Reflected() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
@@ -826,6 +1117,12 @@
         checkTunnelInput(AF_INET, AF_INET, false, false);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testMigrateTunnelV4InV4() throws Exception {
+        doTestMigrateTunnel(AF_INET, AF_INET, false, false);
+    }
+
     @Test
     public void testTunnelV4InV4Reflected() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
@@ -839,6 +1136,12 @@
         checkTunnelInput(AF_INET, AF_INET, true, false);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testMigrateTunnelV4InV4UdpEncap() throws Exception {
+        doTestMigrateTunnel(AF_INET, AF_INET, true, false);
+    }
+
     @Test
     public void testTunnelV4InV4UdpEncapReflected() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
@@ -852,6 +1155,12 @@
         checkTunnelInput(AF_INET, AF_INET6, false, false);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testMigrateTunnelV4InV6() throws Exception {
+        doTestMigrateTunnel(AF_INET, AF_INET6, false, false);
+    }
+
     @Test
     public void testTunnelV4InV6Reflected() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
@@ -865,6 +1174,12 @@
         checkTunnelInput(AF_INET6, AF_INET, false, false);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testMigrateTunnelV6InV4() throws Exception {
+        doTestMigrateTunnel(AF_INET6, AF_INET, false, false);
+    }
+
     @Test
     public void testTunnelV6InV4Reflected() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
@@ -878,6 +1193,12 @@
         checkTunnelInput(AF_INET6, AF_INET, true, false);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testMigrateTunnelV6InV4UdpEncap() throws Exception {
+        doTestMigrateTunnel(AF_INET6, AF_INET, true, false);
+    }
+
     @Test
     public void testTunnelV6InV4UdpEncapReflected() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
@@ -891,6 +1212,12 @@
         checkTunnelInput(AF_INET6, AF_INET6, false, false);
     }
 
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    @Test
+    public void testMigrateTunnelV6InV6() throws Exception {
+        doTestMigrateTunnel(AF_INET6, AF_INET6, false, false);
+    }
+
     @Test
     public void testTunnelV6InV6Reflected() throws Exception {
         assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 5cf7140..c505cef 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -15,6 +15,7 @@
  */
 package android.net.cts
 
+import android.Manifest.permission.NETWORK_SETTINGS
 import android.app.Instrumentation
 import android.content.Context
 import android.net.ConnectivityManager
@@ -65,17 +66,23 @@
 import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
 import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnValidationStatus
 import android.os.Build
+import android.os.Handler
 import android.os.HandlerThread
 import android.os.Looper
 import android.os.Message
+import android.os.SystemClock
+import android.telephony.TelephonyManager
 import android.util.DebugUtils.valueToString
 import androidx.test.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.compatibility.common.util.ThrowingSupplier
 import com.android.modules.utils.build.SdkLevel
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.testutils.CompatUtil
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Losing
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.TestableNetworkCallback
 import org.junit.After
@@ -114,6 +121,7 @@
 // requests filed by the test and should never match normal internet requests. 70 is the default
 // score of Ethernet networks, it's as good a value as any other.
 private const val TEST_NETWORK_SCORE = 70
+private const val WORSE_NETWORK_SCORE = 65
 private const val BETTER_NETWORK_SCORE = 75
 private const val FAKE_NET_ID = 1098
 private val instrumentation: Instrumentation
@@ -262,10 +270,9 @@
             history.add(OnSignalStrengthThresholdsUpdated(thresholds))
         }
 
-        fun expectEmptySignalStrengths() {
+        fun expectSignalStrengths(thresholds: IntArray? = intArrayOf()) {
             expectCallback<OnSignalStrengthThresholdsUpdated>().let {
-                // intArrayOf() without arguments makes an empty array
-                assertArrayEquals(intArrayOf(), it.thresholds)
+                assertArrayEquals(thresholds, it.thresholds)
             }
         }
 
@@ -285,7 +292,7 @@
         // a NetworkAgent whose network does not require validation (which test networks do
         // not, since they lack the INTERNET capability). It always contains the default argument
         // for the URI.
-        fun expectNoInternetValidationStatus() = expectCallback<OnValidationStatus>().let {
+        fun expectValidationBypassedStatus() = expectCallback<OnValidationStatus>().let {
             assertEquals(it.status, VALID_NETWORK)
             // The returned Uri is parsed from the empty string, which means it's an
             // instance of the (private) Uri.StringUri. There are no real good ways
@@ -300,6 +307,11 @@
             return foundCallback
         }
 
+        inline fun <reified T : CallbackEntry> eventuallyExpect() =
+                history.poll(DEFAULT_TIMEOUT_MS) { it is T }.also {
+                    assertNotNull(it, "Callback ${T::class} not received")
+        } as T
+
         fun assertNoCallback() {
             assertTrue(waitForIdle(DEFAULT_TIMEOUT_MS),
                     "Handler didn't became idle after ${DEFAULT_TIMEOUT_MS}ms")
@@ -320,11 +332,33 @@
         callbacksToCleanUp.add(callback)
     }
 
+    private fun registerBestMatchingNetworkCallback(
+        request: NetworkRequest,
+        callback: TestableNetworkCallback,
+        handler: Handler
+    ) {
+        mCM!!.registerBestMatchingNetworkCallback(request, callback, handler)
+        callbacksToCleanUp.add(callback)
+    }
+
+    private fun makeTestNetworkRequest(specifier: String? = null): NetworkRequest {
+        return NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_TEST)
+                .also {
+                    if (specifier != null) {
+                        it.setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifier))
+                    }
+                }
+                .build()
+    }
+
     private fun createNetworkAgent(
         context: Context = realContext,
-        name: String? = null,
+        specifier: String? = null,
         initialNc: NetworkCapabilities? = null,
-        initialLp: LinkProperties? = null
+        initialLp: LinkProperties? = null,
+        initialConfig: NetworkAgentConfig? = null
     ): TestableNetworkAgent {
         val nc = initialNc ?: NetworkCapabilities().apply {
             addTransportType(TRANSPORT_TEST)
@@ -336,33 +370,38 @@
             if (SdkLevel.isAtLeastS()) {
                 addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
             }
-            if (null != name) {
-                setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name))
+            if (null != specifier) {
+                setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifier))
             }
         }
         val lp = initialLp ?: LinkProperties().apply {
             addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
             addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
         }
-        val config = NetworkAgentConfig.Builder().build()
+        val config = initialConfig ?: NetworkAgentConfig.Builder().build()
         return TestableNetworkAgent(context, mHandlerThread.looper, nc, lp, config).also {
             agentsToCleanUp.add(it)
         }
     }
 
-    private fun createConnectedNetworkAgent(context: Context = realContext, name: String? = null):
-            Pair<TestableNetworkAgent, TestableNetworkCallback> {
-        val request: NetworkRequest = NetworkRequest.Builder()
-                .clearCapabilities()
-                .addTransportType(TRANSPORT_TEST)
-                .build()
+    private fun createConnectedNetworkAgent(
+        context: Context = realContext,
+        specifier: String? = UUID.randomUUID().toString(),
+        initialConfig: NetworkAgentConfig? = null,
+        expectedInitSignalStrengthThresholds: IntArray? = intArrayOf()
+    ): Pair<TestableNetworkAgent, TestableNetworkCallback> {
         val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
-        requestNetwork(request, callback)
-        val agent = createNetworkAgent(context, name)
+        // Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
+        requestNetwork(makeTestNetworkRequest(specifier = specifier), callback)
+        val agent = createNetworkAgent(context, specifier, initialConfig = initialConfig)
         agent.setTeardownDelayMillis(0)
+        // Connect the agent and verify initial status callbacks.
         agent.register()
         agent.markConnected()
         agent.expectCallback<OnNetworkCreated>()
+        agent.expectSignalStrengths(expectedInitSignalStrengthThresholds)
+        agent.expectValidationBypassedStatus()
+        callback.expectAvailableThenValidatedCallbacks(agent.network!!)
         return agent to callback
     }
 
@@ -371,29 +410,63 @@
     }
 
     @Test
+    fun testSetSubtypeNameAndExtraInfoByAgentConfig() {
+        val subtypeLTE = TelephonyManager.NETWORK_TYPE_LTE
+        val subtypeNameLTE = "LTE"
+        val legacyExtraInfo = "mylegacyExtraInfo"
+        val config = NetworkAgentConfig.Builder()
+                .setLegacySubType(subtypeLTE)
+                .setLegacySubTypeName(subtypeNameLTE)
+                .setLegacyExtraInfo(legacyExtraInfo).build()
+        val (agent, callback) = createConnectedNetworkAgent(initialConfig = config)
+        val networkInfo = mCM.getNetworkInfo(agent.network)
+        assertEquals(subtypeLTE, networkInfo.getSubtype())
+        assertEquals(subtypeNameLTE, networkInfo.getSubtypeName())
+        assertEquals(legacyExtraInfo, config.getLegacyExtraInfo())
+    }
+
+    @Test
+    fun testSetLegacySubtypeInNetworkAgent() {
+        val subtypeLTE = TelephonyManager.NETWORK_TYPE_LTE
+        val subtypeUMTS = TelephonyManager.NETWORK_TYPE_UMTS
+        val subtypeNameLTE = "LTE"
+        val subtypeNameUMTS = "UMTS"
+        val config = NetworkAgentConfig.Builder()
+                .setLegacySubType(subtypeLTE)
+                .setLegacySubTypeName(subtypeNameLTE).build()
+        val (agent, callback) = createConnectedNetworkAgent(initialConfig = config)
+            agent.setLegacySubtype(subtypeUMTS, subtypeNameUMTS)
+
+            // There is no callback when networkInfo changes,
+            // so use the NetworkCapabilities callback to ensure
+            // that networkInfo is ready for verification.
+            val nc = NetworkCapabilities(agent.nc)
+            nc.addCapability(NET_CAPABILITY_NOT_METERED)
+            agent.sendNetworkCapabilities(nc)
+            callback.expectCapabilitiesThat(agent.network) {
+                it.hasCapability(NET_CAPABILITY_NOT_METERED)
+            }
+            val networkInfo = mCM.getNetworkInfo(agent.network)
+            assertEquals(subtypeUMTS, networkInfo.getSubtype())
+            assertEquals(subtypeNameUMTS, networkInfo.getSubtypeName())
+    }
+
+    @Test
     fun testConnectAndUnregister() {
         val (agent, callback) = createConnectedNetworkAgent()
-        callback.expectAvailableThenValidatedCallbacks(agent.network)
-        agent.expectEmptySignalStrengths()
-        agent.expectNoInternetValidationStatus()
-        agent.unregister()
-        callback.expectCallback<Lost>(agent.network)
-        agent.expectCallback<OnNetworkUnwanted>()
+        unregister(agent)
+        callback.expectCallback<Lost>(agent.network!!)
         assertFailsWith<IllegalStateException>("Must not be able to register an agent twice") {
             agent.register()
         }
-        agent.expectCallback<OnNetworkDestroyed>()
     }
 
     @Test
     fun testOnBandwidthUpdateRequested() {
-        val (agent, callback) = createConnectedNetworkAgent()
-        callback.expectAvailableThenValidatedCallbacks(agent.network)
-        agent.expectEmptySignalStrengths()
-        agent.expectNoInternetValidationStatus()
-        mCM.requestBandwidthUpdate(agent.network)
+        val (agent, _) = createConnectedNetworkAgent()
+        mCM.requestBandwidthUpdate(agent.network!!)
         agent.expectCallback<OnBandwidthUpdateRequested>()
-        agent.unregister()
+        unregister(agent)
     }
 
     @Test
@@ -409,13 +482,8 @@
                 registerNetworkCallback(request, it)
             }
         }
-        createConnectedNetworkAgent().let { (agent, callback) ->
-            callback.expectAvailableThenValidatedCallbacks(agent.network)
-            agent.expectCallback<OnSignalStrengthThresholdsUpdated>().let {
-                assertArrayEquals(it.thresholds, thresholds)
-            }
-            agent.expectNoInternetValidationStatus()
-
+        createConnectedNetworkAgent(expectedInitSignalStrengthThresholds = thresholds).let {
+            (agent, callback) ->
             // Send signal strength and check that the callbacks are called appropriately.
             val nc = NetworkCapabilities(agent.nc)
             nc.setSignalStrength(20)
@@ -424,21 +492,21 @@
 
             nc.setSignalStrength(40)
             agent.sendNetworkCapabilities(nc)
-            callbacks[0].expectAvailableCallbacks(agent.network)
+            callbacks[0].expectAvailableCallbacks(agent.network!!)
             callbacks[1].assertNoCallback(NO_CALLBACK_TIMEOUT)
             callbacks[2].assertNoCallback(NO_CALLBACK_TIMEOUT)
 
             nc.setSignalStrength(80)
             agent.sendNetworkCapabilities(nc)
-            callbacks[0].expectCapabilitiesThat(agent.network) { it.signalStrength == 80 }
-            callbacks[1].expectAvailableCallbacks(agent.network)
-            callbacks[2].expectAvailableCallbacks(agent.network)
+            callbacks[0].expectCapabilitiesThat(agent.network!!) { it.signalStrength == 80 }
+            callbacks[1].expectAvailableCallbacks(agent.network!!)
+            callbacks[2].expectAvailableCallbacks(agent.network!!)
 
             nc.setSignalStrength(55)
             agent.sendNetworkCapabilities(nc)
-            callbacks[0].expectCapabilitiesThat(agent.network) { it.signalStrength == 55 }
-            callbacks[1].expectCapabilitiesThat(agent.network) { it.signalStrength == 55 }
-            callbacks[2].expectCallback<Lost>(agent.network)
+            callbacks[0].expectCapabilitiesThat(agent.network!!) { it.signalStrength == 55 }
+            callbacks[1].expectCapabilitiesThat(agent.network!!) { it.signalStrength == 55 }
+            callbacks[2].expectCallback<Lost>(agent.network!!)
         }
         callbacks.forEach {
             mCM.unregisterNetworkCallback(it)
@@ -487,20 +555,17 @@
 
     @Test
     fun testSendUpdates(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
-        callback.expectAvailableThenValidatedCallbacks(agent.network)
-        agent.expectEmptySignalStrengths()
-        agent.expectNoInternetValidationStatus()
         val ifaceName = "adhocIface"
         val lp = LinkProperties(agent.lp)
         lp.setInterfaceName(ifaceName)
         agent.sendLinkProperties(lp)
-        callback.expectLinkPropertiesThat(agent.network) {
+        callback.expectLinkPropertiesThat(agent.network!!) {
             it.getInterfaceName() == ifaceName
         }
         val nc = NetworkCapabilities(agent.nc)
         nc.addCapability(NET_CAPABILITY_NOT_METERED)
         agent.sendNetworkCapabilities(nc)
-        callback.expectCapabilitiesThat(agent.network) {
+        callback.expectCapabilitiesThat(agent.network!!) {
             it.hasCapability(NET_CAPABILITY_NOT_METERED)
         }
     }
@@ -509,49 +574,32 @@
     fun testSendScore() {
         // This test will create two networks and check that the one with the stronger
         // score wins out for a request that matches them both.
-        // First create requests to make sure both networks are kept up, using the
-        // specifier so they are specific to each network
-        val name1 = UUID.randomUUID().toString()
-        val name2 = UUID.randomUUID().toString()
-        val request1 = NetworkRequest.Builder()
-                .clearCapabilities()
-                .addTransportType(TRANSPORT_TEST)
-                .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name1))
-                .build()
-        val request2 = NetworkRequest.Builder()
-                .clearCapabilities()
-                .addTransportType(TRANSPORT_TEST)
-                .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name2))
-                .build()
-        val callback1 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
-        val callback2 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
-        requestNetwork(request1, callback1)
-        requestNetwork(request2, callback2)
 
-        // Then file the interesting request
-        val request = NetworkRequest.Builder()
-                .clearCapabilities()
-                .addTransportType(TRANSPORT_TEST)
-                .build()
+        // File the interesting request
         val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
-        requestNetwork(request, callback)
+        requestNetwork(makeTestNetworkRequest(), callback)
 
-        // Connect the first Network
-        createConnectedNetworkAgent(name = name1).let { (agent1, _) ->
-            callback.expectAvailableThenValidatedCallbacks(agent1.network)
-            // Upgrade agent1 to a better score so that there is no ambiguity when
-            // agent2 connects that agent1 is still better
-            agent1.sendNetworkScore(BETTER_NETWORK_SCORE - 1)
-            // Connect the second agent
-            createConnectedNetworkAgent(name = name2).let { (agent2, _) ->
-                agent2.markConnected()
-                // The callback should not see anything yet
-                callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
-                // Now update the score and expect the callback now prefers agent2
-                agent2.sendNetworkScore(BETTER_NETWORK_SCORE)
-                callback.expectCallback<Available>(agent2.network)
-            }
-        }
+        // Connect the first Network, with an unused callback that kept the network up.
+        val (agent1, _) = createConnectedNetworkAgent()
+        callback.expectAvailableThenValidatedCallbacks(agent1.network!!)
+        // If using the int ranking, agent1 must be upgraded to a better score so that there is
+        // no ambiguity when agent2 connects that agent1 is still better. If using policy
+        // ranking, this is not necessary.
+        agent1.sendNetworkScore(NetworkScore.Builder().setLegacyInt(BETTER_NETWORK_SCORE)
+                .build())
+
+        // Connect the second agent.
+        val (agent2, _) = createConnectedNetworkAgent()
+        // The callback should not see anything yet. With int ranking, agent1 was upgraded
+        // to a stronger score beforehand. With policy ranking, agent1 is preferred by
+        // virtue of already satisfying the request.
+        callback.assertNoCallback(NO_CALLBACK_TIMEOUT)
+        // Now downgrade the score and expect the callback now prefers agent2
+        agent1.sendNetworkScore(NetworkScore.Builder()
+                .setLegacyInt(WORSE_NETWORK_SCORE)
+                .setExiting(true)
+                .build())
+        callback.expectCallback<Available>(agent2.network!!)
 
         // tearDown() will unregister the requests and agents
     }
@@ -592,12 +640,11 @@
         callback.expectAvailableThenValidatedCallbacks(agent.network!!)
 
         // Check that the default network's transport is propagated to the VPN.
-        var vpnNc = mCM.getNetworkCapabilities(agent.network)
+        var vpnNc = mCM.getNetworkCapabilities(agent.network!!)
         assertNotNull(vpnNc)
         assertEquals(VpnManager.TYPE_VPN_SERVICE,
                 (vpnNc.transportInfo as VpnTransportInfo).type)
-        // TODO: b/183938194 please fix the issue and enable following check.
-        // assertEquals(mySessionId, (vpnNc.transportInfo as VpnTransportInfo).sessionId)
+        assertEquals(mySessionId, (vpnNc.transportInfo as VpnTransportInfo).sessionId)
 
         val testAndVpn = intArrayOf(TRANSPORT_TEST, TRANSPORT_VPN)
         assertTrue(hasAllTransports(vpnNc, testAndVpn))
@@ -625,7 +672,7 @@
         // This is not very accurate because the test does not control the capabilities of the
         // underlying networks, and because not congested, not roaming, and not suspended are the
         // default anyway. It's still useful as an extra check though.
-        vpnNc = mCM.getNetworkCapabilities(agent.network)
+        vpnNc = mCM.getNetworkCapabilities(agent.network!!)
         for (cap in listOf(NET_CAPABILITY_NOT_CONGESTED,
                 NET_CAPABILITY_NOT_ROAMING,
                 NET_CAPABILITY_NOT_SUSPENDED)) {
@@ -635,8 +682,14 @@
             }
         }
 
+        unregister(agent)
+        callback.expectCallback<Lost>(agent.network!!)
+    }
+
+    private fun unregister(agent: TestableNetworkAgent) {
         agent.unregister()
-        callback.expectCallback<Lost>(agent.network)
+        agent.eventuallyExpect<OnNetworkUnwanted>()
+        agent.eventuallyExpect<OnNetworkDestroyed>()
     }
 
     @Test
@@ -718,45 +771,167 @@
     fun testTemporarilyUnmeteredCapability() {
         // This test will create a networks with/without NET_CAPABILITY_TEMPORARILY_NOT_METERED
         // and check that the callback reflects the capability changes.
-        // First create a request to make sure the network is kept up
-        val request1 = NetworkRequest.Builder()
-                .clearCapabilities()
-                .addTransportType(TRANSPORT_TEST)
-                .build()
-        val callback1 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS).also {
-            registerNetworkCallback(request1, it)
-        }
-        requestNetwork(request1, callback1)
-
-        // Then file the interesting request
-        val request = NetworkRequest.Builder()
-                .clearCapabilities()
-                .addTransportType(TRANSPORT_TEST)
-                .build()
-        val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
-        requestNetwork(request, callback)
 
         // Connect the network
-        createConnectedNetworkAgent().let { (agent, _) ->
-            callback.expectAvailableThenValidatedCallbacks(agent.network)
+        val (agent, callback) = createConnectedNetworkAgent()
 
-            // Send TEMP_NOT_METERED and check that the callback is called appropriately.
-            val nc1 = NetworkCapabilities(agent.nc)
-                    .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
-            agent.sendNetworkCapabilities(nc1)
-            callback.expectCapabilitiesThat(agent.network) {
-                it.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
-            }
-
-            // Remove TEMP_NOT_METERED and check that the callback is called appropriately.
-            val nc2 = NetworkCapabilities(agent.nc)
-                    .removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
-            agent.sendNetworkCapabilities(nc2)
-            callback.expectCapabilitiesThat(agent.network) {
-                !it.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
-            }
+        // Send TEMP_NOT_METERED and check that the callback is called appropriately.
+        val nc1 = NetworkCapabilities(agent.nc)
+                .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+        agent.sendNetworkCapabilities(nc1)
+        callback.expectCapabilitiesThat(agent.network!!) {
+            it.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
         }
 
+        // Remove TEMP_NOT_METERED and check that the callback is called appropriately.
+        val nc2 = NetworkCapabilities(agent.nc)
+                .removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+        agent.sendNetworkCapabilities(nc2)
+        callback.expectCapabilitiesThat(agent.network!!) {
+            !it.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+        }
+
+        // tearDown() will unregister the requests and agents
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    fun testSetLingerDuration() {
+        // This test will create two networks and check that the one with the stronger
+        // score wins out for a request that matches them both. And the weaker agent will
+        // be disconnected after customized linger duration.
+
+        // Request the first Network, with a request that could moved to agentStronger in order to
+        // make agentWeaker linger later.
+        val specifierWeaker = UUID.randomUUID().toString()
+        val specifierStronger = UUID.randomUUID().toString()
+        val commonCallback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+        requestNetwork(makeTestNetworkRequest(), commonCallback)
+        val agentWeaker = createNetworkAgent(specifier = specifierWeaker)
+        agentWeaker.register()
+        agentWeaker.markConnected()
+        commonCallback.expectAvailableThenValidatedCallbacks(agentWeaker.network!!)
+        // Downgrade agentWeaker to a worse score so that there is no ambiguity when
+        // agentStronger connects.
+        agentWeaker.sendNetworkScore(NetworkScore.Builder().setLegacyInt(WORSE_NETWORK_SCORE)
+                .setExiting(true).build())
+
+        // Verify invalid linger duration cannot be set.
+        assertFailsWith<IllegalArgumentException> {
+            agentWeaker.setLingerDuration(Duration.ofMillis(-1))
+        }
+        assertFailsWith<IllegalArgumentException> { agentWeaker.setLingerDuration(Duration.ZERO) }
+        assertFailsWith<IllegalArgumentException> {
+            agentWeaker.setLingerDuration(Duration.ofMillis(Integer.MIN_VALUE.toLong()))
+        }
+        assertFailsWith<IllegalArgumentException> {
+            agentWeaker.setLingerDuration(Duration.ofMillis(Integer.MAX_VALUE.toLong() + 1))
+        }
+        assertFailsWith<IllegalArgumentException> {
+            agentWeaker.setLingerDuration(Duration.ofMillis(
+                    NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - 1))
+        }
+        // Verify valid linger timer can be set, but it should not take effect since the network
+        // is still needed.
+        agentWeaker.setLingerDuration(Duration.ofMillis(Integer.MAX_VALUE.toLong()))
+        commonCallback.assertNoCallback(NO_CALLBACK_TIMEOUT)
+        // Set to the value we want to verify the functionality.
+        agentWeaker.setLingerDuration(Duration.ofMillis(NetworkAgent.MIN_LINGER_TIMER_MS.toLong()))
+        // Make a listener which can observe agentWeaker lost later.
+        val callbackWeaker = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+        registerNetworkCallback(NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_TEST)
+                .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifierWeaker))
+                .build(), callbackWeaker)
+        callbackWeaker.expectAvailableCallbacks(agentWeaker.network!!)
+
+        // Connect the agentStronger with a score better than agentWeaker. Verify the callback for
+        // agentWeaker sees the linger expiry while the callback for both sees the winner.
+        // Record linger start timestamp prior to send score to prevent possible race, the actual
+        // timestamp should be slightly late than this since the service handles update
+        // network score asynchronously.
+        val lingerStart = SystemClock.elapsedRealtime()
+        val agentStronger = createNetworkAgent(specifier = specifierStronger)
+        agentStronger.register()
+        agentStronger.markConnected()
+        commonCallback.expectAvailableCallbacks(agentStronger.network!!)
+        callbackWeaker.expectCallback<Losing>(agentWeaker.network!!)
+        val expectedRemainingLingerDuration = lingerStart +
+                NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - SystemClock.elapsedRealtime()
+        // If the available callback is too late. The remaining duration will be reduced.
+        assertTrue(expectedRemainingLingerDuration > 0,
+                "expected remaining linger duration is $expectedRemainingLingerDuration")
+        callbackWeaker.assertNoCallback(expectedRemainingLingerDuration)
+        callbackWeaker.expectCallback<Lost>(agentWeaker.network!!)
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    fun testSetSubscriberId() {
+        val imsi = UUID.randomUUID().toString()
+        val config = NetworkAgentConfig.Builder().setSubscriberId(imsi).build()
+
+        val (agent, _) = createConnectedNetworkAgent(initialConfig = config)
+        val snapshots = runWithShellPermissionIdentity(ThrowingSupplier {
+                mCM!!.allNetworkStateSnapshots }, NETWORK_SETTINGS)
+        val testNetworkSnapshot = snapshots.findLast { it.network == agent.network }
+        assertEquals(imsi, testNetworkSnapshot!!.subscriberId)
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    // TODO: Refactor helper functions to util class and move this test case to
+    //  {@link android.net.cts.ConnectivityManagerTest}.
+    fun testRegisterBestMatchingNetworkCallback() {
+        // Register best matching network callback with additional condition that will be
+        // exercised later. This assumes the test network agent has NOT_VCN_MANAGED in it and
+        // does not have NET_CAPABILITY_TEMPORARILY_NOT_METERED.
+        val bestMatchingCb = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
+        registerBestMatchingNetworkCallback(NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_TEST)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .build(), bestMatchingCb, mHandlerThread.threadHandler)
+
+        val (agent1, _) = createConnectedNetworkAgent(specifier = "AGENT-1")
+        bestMatchingCb.expectAvailableThenValidatedCallbacks(agent1.network!!)
+        // Make agent1 worse so when agent2 shows up, the callback will see that.
+        agent1.sendNetworkScore(NetworkScore.Builder().setExiting(true).build())
+        bestMatchingCb.assertNoCallback(NO_CALLBACK_TIMEOUT)
+
+        val (agent2, _) = createConnectedNetworkAgent(specifier = "AGENT-2")
+        bestMatchingCb.expectAvailableDoubleValidatedCallbacks(agent2.network!!)
+
+        // Change something on agent1 to trigger capabilities changed, since the callback
+        // only cares about the best network, verify it received nothing from agent1.
+        val ncAgent1 = agent1.nc
+        ncAgent1.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+        agent1.sendNetworkCapabilities(ncAgent1)
+        bestMatchingCb.assertNoCallback(NO_CALLBACK_TIMEOUT)
+
+        // Make agent1 the best network again, verify the callback now tracks agent1.
+        agent1.sendNetworkScore(NetworkScore.Builder()
+                .setExiting(false).setTransportPrimary(true).build())
+        bestMatchingCb.expectAvailableCallbacks(agent1.network!!)
+
+        // Make agent1 temporary vcn managed, which will not satisfying the request.
+        // Verify the callback switch from/to the other network accordingly.
+        ncAgent1.removeCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        agent1.sendNetworkCapabilities(ncAgent1)
+        bestMatchingCb.expectAvailableCallbacks(agent2.network!!)
+        ncAgent1.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+        agent1.sendNetworkCapabilities(ncAgent1)
+        bestMatchingCb.expectAvailableDoubleValidatedCallbacks(agent1.network!!)
+
+        // Verify the callback doesn't care about agent2 disconnect.
+        agent2.unregister()
+        agentsToCleanUp.remove(agent2)
+        bestMatchingCb.assertNoCallback()
+        agent1.unregister()
+        agentsToCleanUp.remove(agent1)
+        bestMatchingCb.expectCallback<Lost>(agent1.network!!)
+
         // tearDown() will unregister the requests and agents
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 1a97566..63863da 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -30,6 +30,7 @@
 
 import static junit.framework.Assert.fail;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -327,7 +328,7 @@
     // TODO: 1. Refactor test cases with helper method.
     //       2. Test capability that does not yet exist.
     @Test @IgnoreUpTo(Build.VERSION_CODES.R)
-    public void testBypassingVcnForNonInternetRequest() {
+    public void testBypassingVcn() {
         // Make an empty request. Verify the NOT_VCN_MANAGED is added.
         final NetworkRequest emptyRequest = new NetworkRequest.Builder().build();
         assertTrue(emptyRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
@@ -360,12 +361,12 @@
                 .addCapability(NET_CAPABILITY_NOT_ROAMING).build();
         assertTrue(notRoamRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
 
-        // Make a internet request. Verify the NOT_VCN_MANAGED is added.
+        // Make an internet request. Verify the NOT_VCN_MANAGED is added.
         final NetworkRequest internetRequest = new NetworkRequest.Builder()
                 .addCapability(NET_CAPABILITY_INTERNET).build();
         assertTrue(internetRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
 
-        // Make a internet request which explicitly removed NOT_VCN_MANAGED.
+        // Make an internet request which explicitly removed NOT_VCN_MANAGED.
         // Verify the NOT_VCN_MANAGED is removed.
         final NetworkRequest internetRemoveNotVcnRequest = new NetworkRequest.Builder()
                 .addCapability(NET_CAPABILITY_INTERNET)
@@ -398,6 +399,14 @@
         final NetworkRequest dunRequest = new NetworkRequest.Builder()
                 .addCapability(NET_CAPABILITY_DUN).build();
         assertTrue(dunRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+        // Make an internet request but with NetworkSpecifier. Verify the NOT_VCN_MANAGED is not
+        // added.
+        final NetworkRequest internetWithSpecifierRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).addCapability(NET_CAPABILITY_INTERNET)
+                .setNetworkSpecifier(makeTestWifiSpecifier()).build();
+        assertFalse(internetWithSpecifierRequest.hasCapability(
+                ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
     }
 
     private void verifyEqualRequestBuilt(NetworkRequest orig) {
@@ -410,6 +419,21 @@
         }
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testGetCapabilities() {
+        final int[] netCapabilities = new int[] {
+                NET_CAPABILITY_INTERNET,
+                NET_CAPABILITY_NOT_ROAMING };
+        final NetworkCapabilities.Builder builder = NetworkCapabilities.Builder
+                .withoutDefaultCapabilities();
+        for (int capability : netCapabilities) builder.addCapability(capability);
+        final NetworkRequest nr = new NetworkRequest.Builder()
+                .clearCapabilities()
+                .setCapabilities(builder.build())
+                .build();
+        assertArrayEquals(netCapabilities, nr.getCapabilities());
+    }
+
     @Test
     public void testBuildRequestFromExistingRequestWithBuilder() {
         assumeTrue(TestUtils.shouldTestSApis());
@@ -424,17 +448,20 @@
                 .setSignalStrength(-99).build();
         verifyEqualRequestBuilt(requestCellMms);
 
-        final WifiNetworkSpecifier specifier = new WifiNetworkSpecifier.Builder()
-                .setSsidPattern(new PatternMatcher(TEST_SSID, PatternMatcher.PATTERN_LITERAL))
-                .setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS)
-                .build();
         final NetworkRequest requestWifi = builder
                 .addTransportType(TRANSPORT_WIFI)
                 .removeTransportType(TRANSPORT_CELLULAR)
                 .addCapability(NET_CAPABILITY_INTERNET)
                 .removeCapability(NET_CAPABILITY_MMS)
-                .setNetworkSpecifier(specifier)
+                .setNetworkSpecifier(makeTestWifiSpecifier())
                 .setSignalStrength(-33).build();
         verifyEqualRequestBuilt(requestWifi);
     }
+
+    private WifiNetworkSpecifier makeTestWifiSpecifier() {
+        return new WifiNetworkSpecifier.Builder()
+                .setSsidPattern(new PatternMatcher(TEST_SSID, PatternMatcher.PATTERN_LITERAL))
+                .setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS)
+                .build();
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
index f6fc75b..dde14ac 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
@@ -29,7 +29,7 @@
     /**
      * Clear the test network validation URLs.
      */
-    fun clearValidationTestUrlsDeviceConfig() {
+    @JvmStatic fun clearValidationTestUrlsDeviceConfig() {
         setHttpsUrlDeviceConfig(null)
         setHttpUrlDeviceConfig(null)
         setUrlExpirationDeviceConfig(null)
@@ -40,7 +40,7 @@
      *
      * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
      */
-    fun setHttpsUrlDeviceConfig(url: String?) =
+    @JvmStatic fun setHttpsUrlDeviceConfig(url: String?) =
             setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
 
     /**
@@ -48,7 +48,7 @@
      *
      * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
      */
-    fun setHttpUrlDeviceConfig(url: String?) =
+    @JvmStatic fun setHttpUrlDeviceConfig(url: String?) =
             setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
 
     /**
@@ -56,7 +56,7 @@
      *
      * @see NetworkStackUtils.TEST_URL_EXPIRATION_TIME
      */
-    fun setUrlExpirationDeviceConfig(timestamp: Long?) =
+    @JvmStatic fun setUrlExpirationDeviceConfig(timestamp: Long?) =
             setConfig(NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
 
     private fun setConfig(configKey: String, value: String?) {
diff --git a/tests/cts/net/src/android/net/cts/PacProxyManagerTest.java b/tests/cts/net/src/android/net/cts/PacProxyManagerTest.java
new file mode 100644
index 0000000..7d5e9ff
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/PacProxyManagerTest.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.cts;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Instrumentation;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.PacProxyManager;
+import android.net.Proxy;
+import android.net.ProxyInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+import android.util.Range;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.TestHttpServer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.ServerSocket;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+import fi.iki.elonen.NanoHTTPD.Response.Status;
+
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner.class)
+public final class PacProxyManagerTest {
+    private static final String TAG = PacProxyManagerTest.class.getSimpleName();
+    private static final int PROXY_CHANGED_BROADCAST_TIMEOUT_MS = 5000;
+    private static final int CALLBACK_TIMEOUT_MS = 3 * 1000;
+
+    private Context mContext;
+    private TestHttpServer mServer;
+    private ConnectivityManager mCm;
+    private PacProxyManager mPacProxyManager;
+    private ServerSocket mServerSocket;
+    private Instrumentation mInstrumentation;
+
+    private static final String PAC_FILE = "function FindProxyForURL(url, host)"
+            + "{"
+            + "  return \"PROXY 192.168.0.1:9091\";"
+            + "}";
+
+    @Before
+    public void setUp() throws Exception {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = mInstrumentation.getContext();
+
+        mCm = mContext.getSystemService(ConnectivityManager.class);
+        mPacProxyManager = (PacProxyManager) mContext.getSystemService(PacProxyManager.class);
+        mServer = new TestHttpServer();
+        mServer.start();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mServer != null) {
+            mServer.stop();
+            mServer = null;
+        }
+    }
+
+    private class TestPacProxyInstalledListener implements
+            PacProxyManager.PacProxyInstalledListener {
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+
+        public void onPacProxyInstalled(Network network, ProxyInfo proxy) {
+            Log.e(TAG, "onPacProxyInstalled is called.");
+            mLatch.countDown();
+        }
+
+        public boolean waitForCallback() throws Exception {
+            final boolean result = mLatch.await(CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            return result;
+        }
+    }
+
+    private class ProxyBroadcastReceiver extends BroadcastReceiver {
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+        private final ProxyInfo mProxy;
+
+        ProxyBroadcastReceiver(ProxyInfo proxy) {
+            mProxy = proxy;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final ProxyInfo proxy = (ProxyInfo) intent.getExtra(Proxy.EXTRA_PROXY_INFO,
+                    ProxyInfo.buildPacProxy(Uri.EMPTY));
+            // ProxyTracker sends sticky broadcast which will receive the last broadcast while
+            // register the intent receiver. That is, if system never receives the intent then
+            // it won't receive an intent when register the receiver. How many intents will be
+            // received in the test is unpredictable so here counts down the latch when the PAC
+            // file in the intent is the same as the one at registration.
+            if (mProxy.getPacFileUrl().equals(proxy.getPacFileUrl())) {
+                // Host/Port represent a local proxy server that redirects to the PAC-configured
+                // server. Host should be "localhost" and the port should be a value which is
+                // between 0 and 65535.
+                assertEquals(proxy.getHost(), "localhost");
+                assertInRange(proxy.getPort(), 0 /* lower */, 65535 /* upper */);
+                mLatch.countDown();
+            }
+        }
+
+        public boolean waitForProxyChanged() throws Exception {
+            final boolean result = mLatch.await(PROXY_CHANGED_BROADCAST_TIMEOUT_MS,
+                    TimeUnit.MILLISECONDS);
+            return result;
+        }
+    }
+
+    @Test
+    public void testSetCurrentProxyScriptUrl() throws Exception {
+        // Register a PacProxyInstalledListener
+        final TestPacProxyInstalledListener listener = new TestPacProxyInstalledListener();
+        final Executor executor = (Runnable r) -> r.run();
+
+        runAsShell(NETWORK_SETTINGS, () -> {
+            mPacProxyManager.addPacProxyInstalledListener(executor, listener);
+        });
+
+        final Map<String, String> headers = new HashMap<String, String>();
+        headers.put("Content-Type", "application/x-ns-proxy-autoconfig");
+        final Uri pacProxyUrl = Uri.parse("http://localhost:"
+                + mServer.getListeningPort() + "/proxy.pac");
+        mServer.addResponse(pacProxyUrl, Status.OK, headers, PAC_FILE);
+
+        final ProxyInfo proxy = ProxyInfo.buildPacProxy(pacProxyUrl);
+        final ProxyBroadcastReceiver receiver = new ProxyBroadcastReceiver(proxy);
+        mContext.registerReceiver(receiver, new IntentFilter(Proxy.PROXY_CHANGE_ACTION));
+
+        // Call setCurrentProxyScriptUrl with the URL of the pac file.
+        runAsShell(NETWORK_SETTINGS, () -> {
+            mPacProxyManager.setCurrentProxyScriptUrl(proxy);
+        });
+
+        // Make sure the listener was called and testing the intent is received.
+        try {
+            assertTrue("Didn't receive PROXY_CHANGE_ACTION broadcast.",
+                    receiver.waitForProxyChanged());
+            assertTrue("Did not receive onPacProxyInstalled callback.",
+                    listener.waitForCallback());
+        } finally {
+            runAsShell(NETWORK_SETTINGS, () -> {
+                mPacProxyManager.removePacProxyInstalledListener(listener);
+            });
+            mContext.unregisterReceiver(receiver);
+        }
+    }
+
+    private void assertInRange(int value, int lower, int upper) {
+        final Range range = new Range(lower, upper);
+        assertTrue(value + "is not within range [" + lower + ", " + upper + "]",
+                range.contains(value));
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/PacketUtils.java b/tests/cts/net/src/android/net/cts/PacketUtils.java
index 0aedecb..4d924d1 100644
--- a/tests/cts/net/src/android/net/cts/PacketUtils.java
+++ b/tests/cts/net/src/android/net/cts/PacketUtils.java
@@ -19,6 +19,10 @@
 import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_UDP;
 
+import android.util.ArraySet;
+
+import com.android.internal.net.ipsec.ike.crypto.AesXCbcImpl;
+
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -27,6 +31,7 @@
 import java.security.GeneralSecurityException;
 import java.security.SecureRandom;
 import java.util.Arrays;
+import java.util.Set;
 
 import javax.crypto.Cipher;
 import javax.crypto.Mac;
@@ -43,21 +48,62 @@
     static final int UDP_HDRLEN = 8;
     static final int TCP_HDRLEN = 20;
     static final int TCP_HDRLEN_WITH_TIMESTAMP_OPT = TCP_HDRLEN + 12;
+    static final int ESP_HDRLEN = 8;
+    static final int ESP_BLK_SIZE = 4; // ESP has to be 4-byte aligned
+    static final int ESP_TRAILER_LEN = 2;
 
     // Not defined in OsConstants
     static final int IPPROTO_IPV4 = 4;
     static final int IPPROTO_ESP = 50;
 
     // Encryption parameters
-    static final int AES_GCM_IV_LEN = 8;
     static final int AES_CBC_IV_LEN = 16;
-    static final int AES_GCM_BLK_SIZE = 4;
     static final int AES_CBC_BLK_SIZE = 16;
+    static final int AES_CTR_SALT_LEN = 4;
+
+    static final int AES_CTR_KEY_LEN_20 = 20;
+    static final int AES_CTR_KEY_LEN_28 = 28;
+    static final int AES_CTR_KEY_LEN_36 = 36;
+    static final int AES_CTR_BLK_SIZE = ESP_BLK_SIZE;
+    static final int AES_CTR_IV_LEN = 8;
+
+    // AEAD parameters
+    static final int AES_GCM_IV_LEN = 8;
+    static final int AES_GCM_BLK_SIZE = 4;
+    static final int CHACHA20_POLY1305_KEY_LEN = 36;
+    static final int CHACHA20_POLY1305_BLK_SIZE = ESP_BLK_SIZE;
+    static final int CHACHA20_POLY1305_IV_LEN = 8;
+    static final int CHACHA20_POLY1305_SALT_LEN = 4;
+    static final int CHACHA20_POLY1305_ICV_LEN = 16;
+
+    // Authentication parameters
+    static final int HMAC_SHA256_ICV_LEN = 16;
+    static final int HMAC_SHA512_KEY_LEN = 64;
+    static final int HMAC_SHA512_ICV_LEN = 32;
+    static final int AES_XCBC_KEY_LEN = 16;
+    static final int AES_XCBC_ICV_LEN = 12;
+    static final int AES_CMAC_KEY_LEN = 16;
+    static final int AES_CMAC_ICV_LEN = 12;
+
+    // Block counter field should be 32 bits and starts from value one as per RFC 3686
+    static final byte[] AES_CTR_INITIAL_COUNTER = new byte[] {0x00, 0x00, 0x00, 0x01};
 
     // Encryption algorithms
     static final String AES = "AES";
     static final String AES_CBC = "AES/CBC/NoPadding";
+    static final String AES_CTR = "AES/CTR/NoPadding";
+
+    // AEAD algorithms
+    static final String CHACHA20_POLY1305 = "ChaCha20/Poly1305/NoPadding";
+
+    // Authentication algorithms
+    static final String HMAC_MD5 = "HmacMD5";
+    static final String HMAC_SHA1 = "HmacSHA1";
     static final String HMAC_SHA_256 = "HmacSHA256";
+    static final String HMAC_SHA_384 = "HmacSHA384";
+    static final String HMAC_SHA_512 = "HmacSHA512";
+    static final String AES_CMAC = "AESCMAC";
+    static final String AES_XCBC = "AesXCbc";
 
     public interface Payload {
         byte[] getPacketBytes(IpHeader header) throws Exception;
@@ -310,8 +356,9 @@
         public final int nextHeader;
         public final int spi;
         public final int seqNum;
-        public final byte[] key;
         public final byte[] payload;
+        public final EspCipher cipher;
+        public final EspAuth auth;
 
         /**
          * Generic constructor for ESP headers.
@@ -322,11 +369,48 @@
          * calculated using the pre-encryption IP header
          */
         public EspHeader(int nextHeader, int spi, int seqNum, byte[] key, byte[] payload) {
+            this(nextHeader, spi, seqNum, payload, getDefaultCipher(key), getDefaultAuth(key));
+        }
+
+        /**
+         * Generic constructor for ESP headers that allows configuring encryption and authentication
+         * algortihms.
+         *
+         * <p>For Tunnel mode, payload will be a full IP header + attached payloads
+         *
+         * <p>For Transport mode, payload will be only the attached payloads, but with the checksum
+         * calculated using the pre-encryption IP header
+         */
+        public EspHeader(
+                int nextHeader,
+                int spi,
+                int seqNum,
+                byte[] payload,
+                EspCipher cipher,
+                EspAuth auth) {
             this.nextHeader = nextHeader;
             this.spi = spi;
             this.seqNum = seqNum;
-            this.key = key;
             this.payload = payload;
+            this.cipher = cipher;
+            this.auth = auth;
+
+            if (cipher instanceof EspCipherNull && auth instanceof EspAuthNull) {
+                throw new IllegalArgumentException("No algorithm is provided");
+            }
+
+            if (cipher instanceof EspAeadCipher && !(auth instanceof EspAuthNull)) {
+                throw new IllegalArgumentException(
+                        "AEAD is provided with an authentication" + " algorithm.");
+            }
+        }
+
+        private static EspCipher getDefaultCipher(byte[] key) {
+            return new EspCryptCipher(AES_CBC, AES_CBC_BLK_SIZE, key, AES_CBC_IV_LEN);
+        }
+
+        private static EspAuth getDefaultAuth(byte[] key) {
+            return new EspAuth(HMAC_SHA_256, key, HMAC_SHA256_ICV_LEN);
         }
 
         public int getProtocolId() {
@@ -334,9 +418,10 @@
         }
 
         public short length() {
-            // ALWAYS uses AES-CBC, HMAC-SHA256 (128b trunc len)
-            return (short)
-                    calculateEspPacketSize(payload.length, AES_CBC_IV_LEN, AES_CBC_BLK_SIZE, 128);
+            final int icvLen =
+                    cipher instanceof EspAeadCipher ? ((EspAeadCipher) cipher).icvLen : auth.icvLen;
+            return calculateEspPacketSize(
+                    payload.length, cipher.ivLen, cipher.blockSize, icvLen * 8);
         }
 
         public byte[] getPacketBytes(IpHeader header) throws Exception {
@@ -350,58 +435,12 @@
             ByteBuffer espPayloadBuffer = ByteBuffer.allocate(DATA_BUFFER_LEN);
             espPayloadBuffer.putInt(spi);
             espPayloadBuffer.putInt(seqNum);
-            espPayloadBuffer.put(getCiphertext(key));
 
-            espPayloadBuffer.put(getIcv(getByteArrayFromBuffer(espPayloadBuffer)), 0, 16);
+            espPayloadBuffer.put(cipher.getCipherText(nextHeader, payload, spi, seqNum));
+            espPayloadBuffer.put(auth.getIcv(getByteArrayFromBuffer(espPayloadBuffer)));
+
             resultBuffer.put(getByteArrayFromBuffer(espPayloadBuffer));
         }
-
-        private byte[] getIcv(byte[] authenticatedSection) throws GeneralSecurityException {
-            Mac sha256HMAC = Mac.getInstance(HMAC_SHA_256);
-            SecretKeySpec authKey = new SecretKeySpec(key, HMAC_SHA_256);
-            sha256HMAC.init(authKey);
-
-            return sha256HMAC.doFinal(authenticatedSection);
-        }
-
-        /**
-         * Encrypts and builds ciphertext block. Includes the IV, Padding and Next-Header blocks
-         *
-         * <p>The ciphertext does NOT include the SPI/Sequence numbers, or the ICV.
-         */
-        private byte[] getCiphertext(byte[] key) throws GeneralSecurityException {
-            int paddedLen = calculateEspEncryptedLength(payload.length, AES_CBC_BLK_SIZE);
-            ByteBuffer paddedPayload = ByteBuffer.allocate(paddedLen);
-            paddedPayload.put(payload);
-
-            // Add padding - consecutive integers from 0x01
-            int pad = 1;
-            while (paddedPayload.position() < paddedPayload.limit()) {
-                paddedPayload.put((byte) pad++);
-            }
-
-            paddedPayload.position(paddedPayload.limit() - 2);
-            paddedPayload.put((byte) (paddedLen - 2 - payload.length)); // Pad length
-            paddedPayload.put((byte) nextHeader);
-
-            // Generate Initialization Vector
-            byte[] iv = new byte[AES_CBC_IV_LEN];
-            new SecureRandom().nextBytes(iv);
-            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
-            SecretKeySpec secretKeySpec = new SecretKeySpec(key, AES);
-
-            // Encrypt payload
-            Cipher cipher = Cipher.getInstance(AES_CBC);
-            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
-            byte[] encrypted = cipher.doFinal(getByteArrayFromBuffer(paddedPayload));
-
-            // Build ciphertext
-            ByteBuffer cipherText = ByteBuffer.allocate(AES_CBC_IV_LEN + encrypted.length);
-            cipherText.put(iv);
-            cipherText.put(encrypted);
-
-            return getByteArrayFromBuffer(cipherText);
-        }
     }
 
     private static int addAndWrapForChecksum(int currentChecksum, int value) {
@@ -418,15 +457,14 @@
         return (short) ((~val) & 0xffff);
     }
 
-    public static int calculateEspPacketSize(
+    public static short calculateEspPacketSize(
             int payloadLen, int cryptIvLength, int cryptBlockSize, int authTruncLen) {
-        final int ESP_HDRLEN = 4 + 4; // SPI + Seq#
         final int ICV_LEN = authTruncLen / 8; // Auth trailer; based on truncation length
-        payloadLen += cryptIvLength; // Initialization Vector
 
         // Align to block size of encryption algorithm
         payloadLen = calculateEspEncryptedLength(payloadLen, cryptBlockSize);
-        return payloadLen + ESP_HDRLEN + ICV_LEN;
+        payloadLen += cryptIvLength; // Initialization Vector
+        return (short) (payloadLen + ESP_HDRLEN + ICV_LEN);
     }
 
     private static int calculateEspEncryptedLength(int payloadLen, int cryptBlockSize) {
@@ -457,6 +495,237 @@
         }
     }
 
+    public abstract static class EspCipher {
+        protected static final int SALT_LEN_UNUSED = 0;
+
+        public final String algoName;
+        public final int blockSize;
+        public final byte[] key;
+        public final int ivLen;
+        public final int saltLen;
+        protected byte[] mIv;
+
+        public EspCipher(String algoName, int blockSize, byte[] key, int ivLen, int saltLen) {
+            this.algoName = algoName;
+            this.blockSize = blockSize;
+            this.key = key;
+            this.ivLen = ivLen;
+            this.saltLen = saltLen;
+            this.mIv = getIv(ivLen);
+        }
+
+        public void updateIv(byte[] iv) {
+            this.mIv = iv;
+        }
+
+        public static byte[] getPaddedPayload(int nextHeader, byte[] payload, int blockSize) {
+            final int paddedLen = calculateEspEncryptedLength(payload.length, blockSize);
+            final ByteBuffer paddedPayload = ByteBuffer.allocate(paddedLen);
+            paddedPayload.put(payload);
+
+            // Add padding - consecutive integers from 0x01
+            byte pad = 1;
+            while (paddedPayload.position() < paddedPayload.limit() - ESP_TRAILER_LEN) {
+                paddedPayload.put((byte) pad++);
+            }
+
+            // Add padding length and next header
+            paddedPayload.put((byte) (paddedLen - ESP_TRAILER_LEN - payload.length));
+            paddedPayload.put((byte) nextHeader);
+
+            return getByteArrayFromBuffer(paddedPayload);
+        }
+
+        private static byte[] getIv(int ivLen) {
+            final byte[] iv = new byte[ivLen];
+            new SecureRandom().nextBytes(iv);
+            return iv;
+        }
+
+        public abstract byte[] getCipherText(int nextHeader, byte[] payload, int spi, int seqNum)
+                throws GeneralSecurityException;
+    }
+
+    public static final class EspCipherNull extends EspCipher {
+        private static final String CRYPT_NULL = "CRYPT_NULL";
+        private static final int IV_LEN_UNUSED = 0;
+        private static final byte[] KEY_UNUSED = new byte[0];
+
+        private static final EspCipherNull sInstance = new EspCipherNull();
+
+        private EspCipherNull() {
+            super(CRYPT_NULL, ESP_BLK_SIZE, KEY_UNUSED, IV_LEN_UNUSED, SALT_LEN_UNUSED);
+        }
+
+        public static EspCipherNull getInstance() {
+            return sInstance;
+        }
+
+        @Override
+        public byte[] getCipherText(int nextHeader, byte[] payload, int spi, int seqNum)
+                throws GeneralSecurityException {
+            return getPaddedPayload(nextHeader, payload, blockSize);
+        }
+    }
+
+    public static final class EspCryptCipher extends EspCipher {
+        public EspCryptCipher(String algoName, int blockSize, byte[] key, int ivLen) {
+            this(algoName, blockSize, key, ivLen, SALT_LEN_UNUSED);
+        }
+
+        public EspCryptCipher(String algoName, int blockSize, byte[] key, int ivLen, int saltLen) {
+            super(algoName, blockSize, key, ivLen, saltLen);
+        }
+
+        @Override
+        public byte[] getCipherText(int nextHeader, byte[] payload, int spi, int seqNum)
+                throws GeneralSecurityException {
+            final IvParameterSpec ivParameterSpec;
+            final SecretKeySpec secretKeySpec;
+
+            if (AES_CBC.equals(algoName)) {
+                ivParameterSpec = new IvParameterSpec(mIv);
+                secretKeySpec = new SecretKeySpec(key, algoName);
+            } else if (AES_CTR.equals(algoName)) {
+                // Provided key consists of encryption/decryption key plus 4-byte salt. Salt is used
+                // with ESP payload IV and initial block counter value to build IvParameterSpec.
+                final byte[] secretKey = Arrays.copyOfRange(key, 0, key.length - saltLen);
+                final byte[] salt = Arrays.copyOfRange(key, secretKey.length, key.length);
+                secretKeySpec = new SecretKeySpec(secretKey, algoName);
+
+                final ByteBuffer ivParameterBuffer =
+                        ByteBuffer.allocate(mIv.length + saltLen + AES_CTR_INITIAL_COUNTER.length);
+                ivParameterBuffer.put(salt);
+                ivParameterBuffer.put(mIv);
+                ivParameterBuffer.put(AES_CTR_INITIAL_COUNTER);
+                ivParameterSpec = new IvParameterSpec(ivParameterBuffer.array());
+            } else {
+                throw new IllegalArgumentException("Invalid algorithm " + algoName);
+            }
+
+            // Encrypt payload
+            final Cipher cipher = Cipher.getInstance(algoName);
+            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+            final byte[] encrypted =
+                    cipher.doFinal(getPaddedPayload(nextHeader, payload, blockSize));
+
+            // Build ciphertext
+            final ByteBuffer cipherText = ByteBuffer.allocate(mIv.length + encrypted.length);
+            cipherText.put(mIv);
+            cipherText.put(encrypted);
+
+            return getByteArrayFromBuffer(cipherText);
+        }
+    }
+
+    public static final class EspAeadCipher extends EspCipher {
+        public final int icvLen;
+
+        public EspAeadCipher(
+                String algoName, int blockSize, byte[] key, int ivLen, int icvLen, int saltLen) {
+            super(algoName, blockSize, key, ivLen, saltLen);
+            this.icvLen = icvLen;
+        }
+
+        @Override
+        public byte[] getCipherText(int nextHeader, byte[] payload, int spi, int seqNum)
+                throws GeneralSecurityException {
+            // Provided key consists of encryption/decryption key plus salt. Salt is used
+            // with ESP payload IV to build IvParameterSpec.
+            final byte[] secretKey = Arrays.copyOfRange(key, 0, key.length - saltLen);
+            final byte[] salt = Arrays.copyOfRange(key, secretKey.length, key.length);
+
+            final SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, algoName);
+
+            final ByteBuffer ivParameterBuffer = ByteBuffer.allocate(saltLen + mIv.length);
+            ivParameterBuffer.put(salt);
+            ivParameterBuffer.put(mIv);
+            final IvParameterSpec ivParameterSpec = new IvParameterSpec(ivParameterBuffer.array());
+
+            final ByteBuffer aadBuffer = ByteBuffer.allocate(ESP_HDRLEN);
+            aadBuffer.putInt(spi);
+            aadBuffer.putInt(seqNum);
+
+            // Encrypt payload
+            final Cipher cipher = Cipher.getInstance(algoName);
+            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+            cipher.updateAAD(aadBuffer.array());
+            final byte[] encryptedTextAndIcv =
+                    cipher.doFinal(getPaddedPayload(nextHeader, payload, blockSize));
+
+            // Build ciphertext
+            final ByteBuffer cipherText =
+                    ByteBuffer.allocate(mIv.length + encryptedTextAndIcv.length);
+            cipherText.put(mIv);
+            cipherText.put(encryptedTextAndIcv);
+
+            return getByteArrayFromBuffer(cipherText);
+        }
+    }
+
+    public static class EspAuth {
+        public final String algoName;
+        public final byte[] key;
+        public final int icvLen;
+
+        private static final Set<String> JCE_SUPPORTED_MACS = new ArraySet<>();
+
+        static {
+            JCE_SUPPORTED_MACS.add(HMAC_MD5);
+            JCE_SUPPORTED_MACS.add(HMAC_SHA1);
+            JCE_SUPPORTED_MACS.add(HMAC_SHA_256);
+            JCE_SUPPORTED_MACS.add(HMAC_SHA_384);
+            JCE_SUPPORTED_MACS.add(HMAC_SHA_512);
+            JCE_SUPPORTED_MACS.add(AES_CMAC);
+        }
+
+        public EspAuth(String algoName, byte[] key, int icvLen) {
+            this.algoName = algoName;
+            this.key = key;
+            this.icvLen = icvLen;
+        }
+
+        public byte[] getIcv(byte[] authenticatedSection) throws GeneralSecurityException {
+            if (AES_XCBC.equals(algoName)) {
+                final Cipher aesCipher = Cipher.getInstance(AES_CBC);
+                return new AesXCbcImpl().mac(key, authenticatedSection, true /* needTruncation */);
+            } else if (JCE_SUPPORTED_MACS.contains(algoName)) {
+                final Mac mac = Mac.getInstance(algoName);
+                final SecretKeySpec authKey = new SecretKeySpec(key, algoName);
+                mac.init(authKey);
+
+                final ByteBuffer buffer = ByteBuffer.wrap(mac.doFinal(authenticatedSection));
+                final byte[] icv = new byte[icvLen];
+                buffer.get(icv);
+                return icv;
+            } else {
+                throw new IllegalArgumentException("Invalid algorithm: " + algoName);
+            }
+        }
+    }
+
+    public static final class EspAuthNull extends EspAuth {
+        private static final String AUTH_NULL = "AUTH_NULL";
+        private static final int ICV_LEN_UNUSED = 0;
+        private static final byte[] KEY_UNUSED = new byte[0];
+        private static final byte[] ICV_EMPTY = new byte[0];
+
+        private static final EspAuthNull sInstance = new EspAuthNull();
+
+        private EspAuthNull() {
+            super(AUTH_NULL, KEY_UNUSED, ICV_LEN_UNUSED);
+        }
+
+        public static EspAuthNull getInstance() {
+            return sInstance;
+        }
+
+        @Override
+        public byte[] getIcv(byte[] authenticatedSection) throws GeneralSecurityException {
+            return ICV_EMPTY;
+        }
+    }
+
     /*
      * Debug printing
      */
diff --git a/tests/cts/net/src/android/net/cts/ProxyTest.java b/tests/cts/net/src/android/net/cts/ProxyTest.java
deleted file mode 100644
index 467d12f..0000000
--- a/tests/cts/net/src/android/net/cts/ProxyTest.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.cts;
-
-
-import android.net.Proxy;
-import android.test.AndroidTestCase;
-
-public class ProxyTest extends AndroidTestCase {
-
-    public void testConstructor() {
-        new Proxy();
-    }
-
-    public void testAccessProperties() {
-        final int minValidPort = 0;
-        final int maxValidPort = 65535;
-        int defaultPort = Proxy.getDefaultPort();
-        if(null == Proxy.getDefaultHost()) {
-            assertEquals(-1, defaultPort);
-        } else {
-            assertTrue(defaultPort >= minValidPort && defaultPort <= maxValidPort);
-        }
-    }
-}
diff --git a/tests/cts/net/src/android/net/cts/ProxyTest.kt b/tests/cts/net/src/android/net/cts/ProxyTest.kt
new file mode 100644
index 0000000..a661b26
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/ProxyTest.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.cts
+
+import android.net.ConnectivityManager
+import android.net.Proxy
+import android.net.ProxyInfo
+import android.net.Uri
+import android.os.Build
+import android.text.TextUtils
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import org.junit.Assert
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ProxyTest {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
+    @Test
+    fun testConstructor() {
+        Proxy()
+    }
+
+    @Test
+    fun testAccessProperties() {
+        val minValidPort = 0
+        val maxValidPort = 65535
+        val defaultPort = Proxy.getDefaultPort()
+        if (null == Proxy.getDefaultHost()) {
+            assertEquals(-1, defaultPort.toLong())
+        } else {
+            Assert.assertTrue(defaultPort in minValidPort..maxValidPort)
+        }
+    }
+
+    private fun verifyProxySystemProperties(info: ProxyInfo) {
+        assertEquals(info.host, System.getProperty("http.proxyHost"))
+        assertEquals(info.host, System.getProperty("https.proxyHost"))
+
+        assertEquals(info.port.toString(), System.getProperty("http.proxyPort"))
+        assertEquals(info.port.toString(), System.getProperty("https.proxyPort"))
+
+        val strExcludes = if (info.exclusionList.isEmpty()) null
+                else TextUtils.join("|", info.exclusionList)
+        assertEquals(strExcludes, System.getProperty("https.nonProxyHosts"))
+        assertEquals(strExcludes, System.getProperty("http.nonProxyHosts"))
+    }
+
+    private fun getDefaultProxy(): ProxyInfo? {
+        return InstrumentationRegistry.getInstrumentation().context
+                .getSystemService(ConnectivityManager::class.java)
+                .getDefaultProxy()
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R) // setHttpProxyConfiguration was added in S
+    fun testSetHttpProxyConfiguration_DirectProxy() {
+        val info = ProxyInfo.buildDirectProxy(
+                "testproxy.android.com",
+                12345 /* port */,
+                listOf("testexclude1.android.com", "testexclude2.android.com"))
+        val original = getDefaultProxy()
+        try {
+            Proxy.setHttpProxyConfiguration(info)
+            verifyProxySystemProperties(info)
+        } finally {
+            Proxy.setHttpProxyConfiguration(original)
+        }
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R) // setHttpProxyConfiguration was added in S
+    fun testSetHttpProxyConfiguration_PacProxy() {
+        val pacInfo = ProxyInfo.buildPacProxy(Uri.parse("http://testpac.android.com/pac.pac"))
+        val original = getDefaultProxy()
+        try {
+            Proxy.setHttpProxyConfiguration(pacInfo)
+            verifyProxySystemProperties(pacInfo)
+        } finally {
+            Proxy.setHttpProxyConfiguration(original)
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java b/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java
new file mode 100644
index 0000000..0eb5644
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/TestNetworkRunnable.java
@@ -0,0 +1,132 @@
+/*
+ * 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.cts;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
+import android.net.cts.util.CtsNetUtils;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.ThrowingRunnable;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+/** This class supports running a test with a test network. */
+public class TestNetworkRunnable implements ThrowingRunnable {
+    private static final int IP4_PREFIX_LEN = 32;
+    private static final int IP6_PREFIX_LEN = 128;
+
+    private static final InetAddress DEFAULT_ADDRESS_4 =
+            InetAddress.parseNumericAddress("192.2.0.2");
+    private static final InetAddress DEFAULT_ADDRESS_6 =
+            InetAddress.parseNumericAddress("2001:db8:1::2");
+
+    private static final Context sContext = InstrumentationRegistry.getContext();
+    private static final ConnectivityManager sCm =
+            sContext.getSystemService(ConnectivityManager.class);
+
+    private final Test mTest;
+
+    public TestNetworkRunnable(Test test) {
+        mTest = test;
+    }
+
+    private void runTest() throws Exception {
+        final TestNetworkManager tnm = sContext.getSystemService(TestNetworkManager.class);
+
+        // Non-final; these variables ensure we clean up properly after our test if we
+        // have allocated test network resources
+        TestNetworkInterface testIface = null;
+        TestNetworkCallback tunNetworkCallback = null;
+
+        final CtsNetUtils ctsNetUtils = new CtsNetUtils(sContext);
+        final InetAddress[] addresses = mTest.getTestNetworkAddresses();
+        final LinkAddress[] linkAddresses = new LinkAddress[addresses.length];
+        for (int i = 0; i < addresses.length; i++) {
+            InetAddress address = addresses[i];
+            if (address instanceof Inet4Address) {
+                linkAddresses[i] = new LinkAddress(address, IP4_PREFIX_LEN);
+            } else {
+                linkAddresses[i] = new LinkAddress(address, IP6_PREFIX_LEN);
+            }
+        }
+
+        try {
+            // Build underlying test network
+            testIface = tnm.createTunInterface(linkAddresses);
+
+            // Hold on to this callback to ensure network does not get reaped.
+            tunNetworkCallback = ctsNetUtils.setupAndGetTestNetwork(testIface.getInterfaceName());
+
+            mTest.runTest(testIface, tunNetworkCallback);
+        } finally {
+            try {
+                mTest.cleanupTest();
+            } catch (Exception e) {
+                // No action
+            }
+
+            if (testIface != null) {
+                testIface.getFileDescriptor().close();
+            }
+
+            if (tunNetworkCallback != null) {
+                sCm.unregisterNetworkCallback(tunNetworkCallback);
+            }
+
+            final Network testNetwork = tunNetworkCallback.currentNetwork;
+            if (testNetwork != null) {
+                tnm.teardownTestNetwork(testNetwork);
+            }
+        }
+    }
+
+    @Override
+    public void run() throws Exception {
+        if (sContext.checkSelfPermission(MANAGE_TEST_NETWORKS) == PERMISSION_GRANTED) {
+            runTest();
+        } else {
+            runWithShellPermissionIdentity(this::runTest, MANAGE_TEST_NETWORKS);
+        }
+    }
+
+    /** Interface for test caller to configure the test that will be run with a test network */
+    public interface Test {
+        /** Runs the test with a test network */
+        void runTest(TestNetworkInterface testIface, TestNetworkCallback tunNetworkCallback)
+                throws Exception;
+
+        /** Cleans up when the test is finished or interrupted */
+        void cleanupTest();
+
+        /** Returns the IP addresses that will be used by the test network */
+        default InetAddress[] getTestNetworkAddresses() {
+            return new InetAddress[] {DEFAULT_ADDRESS_4, DEFAULT_ADDRESS_6};
+        }
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/TunUtils.java b/tests/cts/net/src/android/net/cts/TunUtils.java
index 7887385..d8e39b4 100644
--- a/tests/cts/net/src/android/net/cts/TunUtils.java
+++ b/tests/cts/net/src/android/net/cts/TunUtils.java
@@ -147,6 +147,10 @@
         return espPkt; // We've found the packet we're looking for.
     }
 
+    public byte[] awaitEspPacket(int spi, boolean useEncap) throws Exception {
+        return awaitPacket((pkt) -> isEsp(pkt, spi, useEncap));
+    }
+
     private static boolean isSpiEqual(byte[] pkt, int espOffset, int spi) {
         // Check SPI byte by byte.
         return pkt[espOffset] == (byte) ((spi >>> 24) & 0xff)
diff --git a/tests/cts/net/util/Android.bp b/tests/cts/net/util/Android.bp
index 88a2068..fffd30f 100644
--- a/tests/cts/net/util/Android.bp
+++ b/tests/cts/net/util/Android.bp
@@ -26,5 +26,7 @@
         "compatibility-device-util-axt",
         "junit",
         "net-tests-utils",
+        "modules-utils-build",
+        "net-utils-framework-common",
     ],
 }
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index 88172d7..bce9880 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -18,12 +18,12 @@
 
 import static android.Manifest.permission.ACCESS_WIFI_STATE;
 import static android.Manifest.permission.NETWORK_SETTINGS;
-import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION;
 
+import static com.android.compatibility.common.util.PropertyUtil.getFirstApiLevel;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
 
 import static org.junit.Assert.assertEquals;
@@ -56,13 +56,13 @@
 import android.os.Binder;
 import android.os.Build;
 import android.os.IBinder;
-import android.os.SystemProperties;
-import android.provider.Settings;
 import android.system.Os;
 import android.system.OsConstants;
+import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.compatibility.common.util.SystemUtil;
+import com.android.net.module.util.ConnectivitySettingsUtils;
 
 import junit.framework.AssertionFailedError;
 
@@ -81,12 +81,13 @@
 
 public final class CtsNetUtils {
     private static final String TAG = CtsNetUtils.class.getSimpleName();
-    private static final int DURATION = 10000;
     private static final int SOCKET_TIMEOUT_MS = 2000;
     private static final int PRIVATE_DNS_PROBE_MS = 1_000;
 
     private static final int PRIVATE_DNS_SETTING_TIMEOUT_MS = 10_000;
     private static final int CONNECTIVITY_CHANGE_TIMEOUT_SECS = 30;
+    private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
+    private static final String PRIVATE_DNS_MODE_STRICT = "hostname";
     public static final int HTTP_PORT = 80;
     public static final String TEST_HOST = "connectivitycheck.gstatic.com";
     public static final String HTTP_REQUEST =
@@ -103,7 +104,7 @@
     private final ContentResolver mCR;
     private final WifiManager mWifiManager;
     private TestNetworkCallback mCellNetworkCallback;
-    private String mOldPrivateDnsMode;
+    private int mOldPrivateDnsMode = 0;
     private String mOldPrivateDnsSpecifier;
 
     public CtsNetUtils(Context context) {
@@ -116,8 +117,7 @@
     /** Checks if FEATURE_IPSEC_TUNNELS is enabled on the device */
     public boolean hasIpsecTunnelsFeature() {
         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
-                || SystemProperties.getInt("ro.product.first_api_level", 0)
-                        >= Build.VERSION_CODES.Q;
+                || getFirstApiLevel() >= Build.VERSION_CODES.Q;
     }
 
     /**
@@ -508,56 +508,69 @@
     }
 
     public void storePrivateDnsSetting() {
-        // Store private DNS setting
-        mOldPrivateDnsMode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
-        mOldPrivateDnsSpecifier = Settings.Global.getString(mCR,
-                Settings.Global.PRIVATE_DNS_SPECIFIER);
-        // It's possible that there is no private DNS default value in Settings.
-        // Give it a proper default mode which is opportunistic mode.
-        if (mOldPrivateDnsMode == null) {
-            mOldPrivateDnsSpecifier = "";
-            mOldPrivateDnsMode = PRIVATE_DNS_MODE_OPPORTUNISTIC;
-            Settings.Global.putString(mCR,
-                    Settings.Global.PRIVATE_DNS_SPECIFIER, mOldPrivateDnsSpecifier);
-            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldPrivateDnsMode);
-        }
+        mOldPrivateDnsMode = ConnectivitySettingsUtils.getPrivateDnsMode(mContext);
+        mOldPrivateDnsSpecifier = ConnectivitySettingsUtils.getPrivateDnsHostname(mContext);
     }
 
     public void restorePrivateDnsSetting() throws InterruptedException {
-        if (mOldPrivateDnsMode == null || mOldPrivateDnsSpecifier == null) {
+        if (mOldPrivateDnsMode == 0) {
+            fail("restorePrivateDnsSetting without storing settings first");
+        }
+
+        if (mOldPrivateDnsMode != ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) {
+            ConnectivitySettingsUtils.setPrivateDnsMode(mContext, mOldPrivateDnsMode);
             return;
         }
         // restore private DNS setting
-        if ("hostname".equals(mOldPrivateDnsMode)) {
-            setPrivateDnsStrictMode(mOldPrivateDnsSpecifier);
-            awaitPrivateDnsSetting("restorePrivateDnsSetting timeout",
-                    mCm.getActiveNetwork(),
-                    mOldPrivateDnsSpecifier, true);
-        } else {
-            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, mOldPrivateDnsMode);
+        // In case of invalid setting, set to opportunistic to avoid a bad state and fail
+        if (TextUtils.isEmpty(mOldPrivateDnsSpecifier)) {
+            ConnectivitySettingsUtils.setPrivateDnsMode(mContext,
+                    ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OPPORTUNISTIC);
+            fail("Invalid private DNS setting: no hostname specified in strict mode");
         }
+        setPrivateDnsStrictMode(mOldPrivateDnsSpecifier);
+
+        // There might be a race before private DNS setting is applied and the next test is
+        // running. So waiting private DNS to be validated can reduce the flaky rate of test.
+        awaitPrivateDnsSetting("restorePrivateDnsSetting timeout",
+                mCm.getActiveNetwork(),
+                mOldPrivateDnsSpecifier, true /* requiresValidatedServer */);
     }
 
     public void setPrivateDnsStrictMode(String server) {
         // To reduce flake rate, set PRIVATE_DNS_SPECIFIER before PRIVATE_DNS_MODE. This ensures
-        // that if the previous private DNS mode was not "hostname", the system only sees one
+        // that if the previous private DNS mode was not strict, the system only sees one
         // EVENT_PRIVATE_DNS_SETTINGS_CHANGED event instead of two.
-        Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_SPECIFIER, server);
-        final String mode = Settings.Global.getString(mCR, Settings.Global.PRIVATE_DNS_MODE);
-        // If current private DNS mode is "hostname", we only need to set PRIVATE_DNS_SPECIFIER.
-        if (!"hostname".equals(mode)) {
-            Settings.Global.putString(mCR, Settings.Global.PRIVATE_DNS_MODE, "hostname");
+        ConnectivitySettingsUtils.setPrivateDnsHostname(mContext, server);
+        final int mode = ConnectivitySettingsUtils.getPrivateDnsMode(mContext);
+        // If current private DNS mode is strict, we only need to set PRIVATE_DNS_SPECIFIER.
+        if (mode != ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) {
+            ConnectivitySettingsUtils.setPrivateDnsMode(mContext,
+                    ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
         }
     }
 
+    /**
+     * Waiting for the new private DNS setting to be validated.
+     * This method is helpful when the new private DNS setting is configured and ensure the new
+     * setting is applied and workable. It can also reduce the flaky rate when the next test is
+     * running.
+     *
+     * @param msg A message that will be printed when the validation of private DNS is timeout.
+     * @param network A network which will apply the new private DNS setting.
+     * @param server The hostname of private DNS.
+     * @param requiresValidatedServer A boolean to decide if it's needed to wait private DNS to be
+     *                                 validated or not.
+     * @throws InterruptedException If the thread is interrupted.
+     */
     public void awaitPrivateDnsSetting(@NonNull String msg, @NonNull Network network,
-            @NonNull String server, boolean requiresValidatedServers) throws InterruptedException {
-        CountDownLatch latch = new CountDownLatch(1);
-        NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
+            @NonNull String server, boolean requiresValidatedServer) throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
         NetworkCallback callback = new NetworkCallback() {
             @Override
             public void onLinkPropertiesChanged(Network n, LinkProperties lp) {
-                if (requiresValidatedServers && lp.getValidatedPrivateDnsServers().isEmpty()) {
+                if (requiresValidatedServer && lp.getValidatedPrivateDnsServers().isEmpty()) {
                     return;
                 }
                 if (network.equals(n) && server.equals(lp.getPrivateDnsServerName())) {
@@ -577,7 +590,7 @@
         // private DNS probe. There is no way to know when the probe has completed: because the
         // network is likely already validated, there is no callback that we can listen to, so
         // just sleep.
-        if (requiresValidatedServers) {
+        if (requiresValidatedServer) {
             Thread.sleep(PRIVATE_DNS_PROBE_MS);
         }
     }
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
index c95dc28..c220326 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java
@@ -22,7 +22,7 @@
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
 
-import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -35,6 +35,7 @@
 import android.content.pm.PackageManager;
 import android.net.Network;
 import android.net.TetheredClient;
+import android.net.TetheringInterface;
 import android.net.TetheringManager;
 import android.net.TetheringManager.TetheringEventCallback;
 import android.net.TetheringManager.TetheringInterfaceRegexps;
@@ -51,6 +52,7 @@
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 
 public final class CtsTetheringUtils {
     private TetheringManager mTm;
@@ -116,23 +118,38 @@
         }
     }
 
-    public static boolean isIfaceMatch(final List<String> ifaceRegexs, final List<String> ifaces) {
-        return isIfaceMatch(ifaceRegexs.toArray(new String[0]), ifaces);
-    }
-
-    public static boolean isIfaceMatch(final String[] ifaceRegexs, final List<String> ifaces) {
+    private static boolean isRegexMatch(final String[] ifaceRegexs, String iface) {
         if (ifaceRegexs == null) fail("ifaceRegexs should not be null");
 
+        for (String regex : ifaceRegexs) {
+            if (iface.matches(regex)) return true;
+        }
+
+        return false;
+    }
+
+    public static boolean isAnyIfaceMatch(final String[] ifaceRegexs, final List<String> ifaces) {
         if (ifaces == null) return false;
 
         for (String s : ifaces) {
-            for (String regex : ifaceRegexs) {
-                if (s.matches(regex)) {
-                    return true;
-                }
+            if (isRegexMatch(ifaceRegexs, s)) return true;
+        }
+
+        return false;
+    }
+
+    private static TetheringInterface getFirstMatchingTetheringInterface(final List<String> regexs,
+            final int type, final Set<TetheringInterface> ifaces) {
+        if (ifaces == null || regexs == null) return null;
+
+        final String[] regexArray = regexs.toArray(new String[0]);
+        for (TetheringInterface iface : ifaces) {
+            if (isRegexMatch(regexArray, iface.getInterface()) && type == iface.getType()) {
+                return iface;
             }
         }
-        return false;
+
+        return null;
     }
 
     // Must poll the callback before looking at the member.
@@ -171,6 +188,8 @@
         private TetheringInterfaceRegexps mTetherableRegex;
         private List<String> mTetherableIfaces;
         private List<String> mTetheredIfaces;
+        private String mErrorIface;
+        private int mErrorCode;
 
         @Override
         public void onTetheringSupported(boolean supported) {
@@ -191,17 +210,41 @@
         @Override
         public void onTetherableInterfacesChanged(List<String> interfaces) {
             mTetherableIfaces = interfaces;
+        }
+        // Call the interface default implementation, which will call
+        // onTetherableInterfacesChanged(List<String>). This ensures that the default implementation
+        // of the new callback method calls the old callback method and avoids the need to convert
+        // Set<TetheringInterface> to List<String> in this code.
+        @Override
+        public void onTetherableInterfacesChanged(Set<TetheringInterface> interfaces) {
+            TetheringEventCallback.super.onTetherableInterfacesChanged(interfaces);
+            assertHasAllTetheringInterfaces(interfaces, mTetherableIfaces);
             mHistory.add(new CallbackValue(CallbackType.ON_TETHERABLE_IFACES, interfaces, 0));
         }
 
         @Override
         public void onTetheredInterfacesChanged(List<String> interfaces) {
             mTetheredIfaces = interfaces;
+        }
+
+        @Override
+        public void onTetheredInterfacesChanged(Set<TetheringInterface> interfaces) {
+            TetheringEventCallback.super.onTetheredInterfacesChanged(interfaces);
+            assertHasAllTetheringInterfaces(interfaces, mTetheredIfaces);
             mHistory.add(new CallbackValue(CallbackType.ON_TETHERED_IFACES, interfaces, 0));
         }
 
         @Override
         public void onError(String ifName, int error) {
+            mErrorIface = ifName;
+            mErrorCode = error;
+        }
+
+        @Override
+        public void onError(TetheringInterface ifName, int error) {
+            TetheringEventCallback.super.onError(ifName, error);
+            assertEquals(ifName.getInterface(), mErrorIface);
+            assertEquals(error, mErrorCode);
             mHistory.add(new CallbackValue(CallbackType.ON_ERROR, ifName, error));
         }
 
@@ -215,30 +258,66 @@
             mHistory.add(new CallbackValue(CallbackType.ON_OFFLOAD_STATUS, status, 0));
         }
 
-        public void expectTetherableInterfacesChanged(@NonNull List<String> regexs) {
+        private void assertHasAllTetheringInterfaces(Set<TetheringInterface> tetheringIfaces,
+                List<String> ifaces) {
+            // This does not check that the interfaces are the same. This checks that the
+            // List<String> has all the interface names contained by the Set<TetheringInterface>.
+            assertEquals(tetheringIfaces.size(), ifaces.size());
+            for (TetheringInterface tether : tetheringIfaces) {
+                assertTrue("iface " + tether.getInterface()
+                        + " seen by new callback but not old callback",
+                        ifaces.contains(tether.getInterface()));
+            }
+        }
+
+        public void expectTetherableInterfacesChanged(@NonNull final List<String> regexs,
+                final int type) {
             assertNotNull("No expected tetherable ifaces callback", mCurrent.poll(TIMEOUT_MS,
                 (cv) -> {
                     if (cv.callbackType != CallbackType.ON_TETHERABLE_IFACES) return false;
-                    final List<String> interfaces = (List<String>) cv.callbackParam;
-                    return isIfaceMatch(regexs, interfaces);
+                    final Set<TetheringInterface> interfaces =
+                            (Set<TetheringInterface>) cv.callbackParam;
+                    return getFirstMatchingTetheringInterface(regexs, type, interfaces) != null;
                 }));
         }
 
-        public void expectTetheredInterfacesChanged(@NonNull List<String> regexs) {
-            assertNotNull("No expected tethered ifaces callback", mCurrent.poll(TIMEOUT_MS,
-                (cv) -> {
-                    if (cv.callbackType != CallbackType.ON_TETHERED_IFACES) return false;
+        public void expectNoTetheringActive() {
+            assertNotNull("At least one tethering type unexpectedly active",
+                    mCurrent.poll(TIMEOUT_MS, (cv) -> {
+                        if (cv.callbackType != CallbackType.ON_TETHERED_IFACES) return false;
 
-                    final List<String> interfaces = (List<String>) cv.callbackParam;
+                        return ((Set<TetheringInterface>) cv.callbackParam).isEmpty();
+                    }));
+        }
 
-                    // Null regexs means no active tethering.
-                    if (regexs == null) return interfaces.isEmpty();
+        public TetheringInterface expectTetheredInterfacesChanged(
+                @NonNull final List<String> regexs, final int type) {
+            while (true) {
+                final CallbackValue cv = mCurrent.poll(TIMEOUT_MS, c -> true);
+                if (cv == null) {
+                    fail("No expected tethered ifaces callback, expected type: " + type);
+                }
 
-                    return isIfaceMatch(regexs, interfaces);
-                }));
+                if (cv.callbackType != CallbackType.ON_TETHERED_IFACES) continue;
+
+                final Set<TetheringInterface> interfaces =
+                        (Set<TetheringInterface>) cv.callbackParam;
+
+                final TetheringInterface iface =
+                        getFirstMatchingTetheringInterface(regexs, type, interfaces);
+
+                if (iface != null) return iface;
+            }
         }
 
         public void expectCallbackStarted() {
+            // This method uses its own readhead because it just check whether last tethering status
+            // is updated after TetheringEventCallback get registered but do not check content
+            // of received callbacks. Using shared readhead (mCurrent) only when the callbacks the
+            // method polled is also not necessary for other methods which using shared readhead.
+            // All of methods using mCurrent is order mattered.
+            final ArrayTrackRecord<CallbackValue>.ReadHead history =
+                    mHistory.newReadHead();
             int receivedBitMap = 0;
             // The each bit represent a type from CallbackType.ON_*.
             // Expect all of callbacks except for ON_ERROR.
@@ -246,7 +325,7 @@
             // Receive ON_ERROR on started callback is not matter. It just means tethering is
             // failed last time, should able to continue the test this time.
             while ((receivedBitMap & expectedBitMap) != expectedBitMap) {
-                final CallbackValue cv = mCurrent.poll(TIMEOUT_MS, c -> true);
+                final CallbackValue cv = history.poll(TIMEOUT_MS, c -> true);
                 if (cv == null) {
                     fail("No expected callbacks, " + "expected bitmap: "
                             + expectedBitMap + ", actual: " + receivedBitMap);
@@ -269,14 +348,14 @@
             }));
         }
 
-        public void expectErrorOrTethered(final String iface) {
+        public void expectErrorOrTethered(final TetheringInterface iface) {
             assertNotNull("No expected callback", mCurrent.poll(TIMEOUT_MS, (cv) -> {
                 if (cv.callbackType == CallbackType.ON_ERROR
-                        && iface.equals((String) cv.callbackParam)) {
+                        && iface.equals((TetheringInterface) cv.callbackParam)) {
                     return true;
                 }
                 if (cv.callbackType == CallbackType.ON_TETHERED_IFACES
-                        && ((List<String>) cv.callbackParam).contains(iface)) {
+                        && ((Set<TetheringInterface>) cv.callbackParam).contains(iface)) {
                     return true;
                 }
 
@@ -309,33 +388,12 @@
             assumeTetheringSupported();
 
             assumeTrue(!getTetheringInterfaceRegexps().getTetherableWifiRegexs().isEmpty());
-
-            final PackageManager pm = ctx.getPackageManager();
-            assumeTrue(pm.hasSystemFeature(PackageManager.FEATURE_WIFI));
-
-            WifiManager wm = ctx.getSystemService(WifiManager.class);
-            // Wifi feature flags only work when wifi is on.
-            final boolean previousWifiEnabledState = wm.isWifiEnabled();
-            try {
-                if (!previousWifiEnabledState) SystemUtil.runShellCommand("svc wifi enable");
-                waitForWifiEnabled(ctx);
-                assumeTrue(wm.isPortableHotspotSupported());
-            } finally {
-                if (!previousWifiEnabledState) SystemUtil.runShellCommand("svc wifi disable");
-            }
+            assumeTrue(isPortableHotspotSupported(ctx));
         }
 
         public TetheringInterfaceRegexps getTetheringInterfaceRegexps() {
             return mTetherableRegex;
         }
-
-        public List<String> getTetherableInterfaces() {
-            return mTetherableIfaces;
-        }
-
-        public List<String> getTetheredInterfaces() {
-            return mTetheredIfaces;
-        }
     }
 
     private static void waitForWifiEnabled(final Context ctx) throws Exception {
@@ -382,14 +440,31 @@
         return callback.getTetheringInterfaceRegexps().getTetherableWifiRegexs();
     }
 
-    public static boolean isWifiTetheringSupported(final TestTetheringEventCallback callback) {
-        return !getWifiTetherableInterfaceRegexps(callback).isEmpty();
+    public static boolean isWifiTetheringSupported(final Context ctx,
+            final TestTetheringEventCallback callback) throws Exception {
+        return !getWifiTetherableInterfaceRegexps(callback).isEmpty()
+                && isPortableHotspotSupported(ctx);
     }
 
-    public void startWifiTethering(final TestTetheringEventCallback callback)
+    /* Returns if wifi supports hotspot. */
+    private static boolean isPortableHotspotSupported(final Context ctx) throws Exception {
+        final PackageManager pm = ctx.getPackageManager();
+        if (!pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) return false;
+        final WifiManager wm = ctx.getSystemService(WifiManager.class);
+        // Wifi feature flags only work when wifi is on.
+        final boolean previousWifiEnabledState = wm.isWifiEnabled();
+        try {
+            if (!previousWifiEnabledState) SystemUtil.runShellCommand("svc wifi enable");
+            waitForWifiEnabled(ctx);
+            return wm.isPortableHotspotSupported();
+        } finally {
+            if (!previousWifiEnabledState) SystemUtil.runShellCommand("svc wifi disable");
+        }
+    }
+
+    public TetheringInterface startWifiTethering(final TestTetheringEventCallback callback)
             throws InterruptedException {
         final List<String> wifiRegexs = getWifiTetherableInterfaceRegexps(callback);
-        assertFalse(isIfaceMatch(wifiRegexs, callback.getTetheredInterfaces()));
 
         final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
         final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
@@ -397,11 +472,14 @@
         mTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
         startTetheringCallback.verifyTetheringStarted();
 
-        callback.expectTetheredInterfacesChanged(wifiRegexs);
+        final TetheringInterface iface =
+                callback.expectTetheredInterfacesChanged(wifiRegexs, TETHERING_WIFI);
 
         callback.expectOneOfOffloadStatusChanged(
                 TETHER_HARDWARE_OFFLOAD_STARTED,
                 TETHER_HARDWARE_OFFLOAD_FAILED);
+
+        return iface;
     }
 
     private static class StopSoftApCallback implements SoftApCallback {
@@ -441,7 +519,7 @@
     public void stopWifiTethering(final TestTetheringEventCallback callback) {
         mTm.stopTethering(TETHERING_WIFI);
         expectSoftApDisabled();
-        callback.expectTetheredInterfacesChanged(null);
+        callback.expectNoTetheringActive();
         callback.expectOneOfOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
     }
 }
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index fa52e9b..52ce83a 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -16,8 +16,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-android_test {
-    name: "CtsTetheringTest",
+java_defaults {
+    name: "CtsTetheringTestDefaults",
     defaults: ["cts_defaults"],
 
     libs: [
@@ -30,7 +30,6 @@
 
     static_libs: [
         "TetheringCommonTests",
-        "TetheringIntegrationTestsLib",
         "compatibility-device-util-axt",
         "cts-net-utils",
         "net-tests-utils",
@@ -47,14 +46,53 @@
 
     // Change to system current when TetheringManager move to bootclass path.
     platform_apis: true,
+}
+
+// Tethering CTS tests that target the latest released SDK. These tests can be installed on release
+// devices which has equal or lowner sdk version than target sdk and are useful for qualifying
+// mainline modules on release devices.
+android_test {
+    name: "CtsTetheringTestLatestSdk",
+    defaults: ["CtsTetheringTestDefaults"],
+
+    min_sdk_version: "30",
+    target_sdk_version: "30",
+
+    static_libs: [
+        "TetheringIntegrationTestsLatestSdkLib",
+    ],
+
+    test_suites: [
+        "general-tests",
+        "mts-tethering",
+    ],
+
+    test_config_template: "AndroidTestTemplate.xml",
+
+    // Include both the 32 and 64 bit versions
+    compile_multilib: "both",
+}
+
+// Tethering CTS tests for development and release. These tests always target the platform SDK
+// version, and are subject to all the restrictions appropriate to that version. Before SDK
+// finalization, these tests have a min_sdk_version of 10000, and cannot be installed on release
+// devices.
+android_test {
+    name: "CtsTetheringTest",
+    defaults: ["CtsTetheringTestDefaults"],
+
+    static_libs: [
+        "TetheringIntegrationTestsLib",
+    ],
 
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
         "general-tests",
-        "mts-tethering",
     ],
 
+    test_config_template: "AndroidTestTemplate.xml",
+
     // Include both the 32 and 64 bit versions
     compile_multilib: "both",
 }
diff --git a/tests/cts/tethering/AndroidTest.xml b/tests/cts/tethering/AndroidTest.xml
deleted file mode 100644
index e752e3a..0000000
--- a/tests/cts/tethering/AndroidTest.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?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 Tethering test cases">
-    <option name="test-suite-tag" value="cts" />
-    <option name="config-descriptor:metadata" key="component" value="networking" />
-    <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
-    <option name="config-descriptor:metadata" key="parameter" value="not_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="CtsTetheringTest.apk" />
-    </target_preparer>
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
-        <option name="package" value="android.tethering.cts" />
-    </test>
-
-    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
-        <option name="mainline-module-package-name" value="com.google.android.tethering" />
-    </object>
-</configuration>
diff --git a/tests/cts/tethering/AndroidTestTemplate.xml b/tests/cts/tethering/AndroidTestTemplate.xml
new file mode 100644
index 0000000..491b004
--- /dev/null
+++ b/tests/cts/tethering/AndroidTestTemplate.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for {MODULE}">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="networking" />
+    <option name="config-descriptor:metadata" key="token" value="SIM_CARD" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_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="{MODULE}.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.tethering.cts" />
+    </test>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.tethering" />
+    </object>
+</configuration>
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 71a81ff..0a5e506 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -26,8 +26,7 @@
 import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
 import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
-import static android.net.cts.util.CtsTetheringUtils.isIfaceMatch;
-import static android.net.cts.util.CtsTetheringUtils.isWifiTetheringSupported;
+import static android.net.cts.util.CtsTetheringUtils.isAnyIfaceMatch;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -48,6 +47,7 @@
 import android.net.LinkAddress;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.TetheringInterface;
 import android.net.TetheringManager;
 import android.net.TetheringManager.OnTetheringEntitlementResultListener;
 import android.net.TetheringManager.TetheringInterfaceRegexps;
@@ -190,13 +190,13 @@
         }
 
         private boolean isIfaceActive(final String[] ifaceRegexs, final TetherState state) {
-            return isIfaceMatch(ifaceRegexs, state.mActive);
+            return isAnyIfaceMatch(ifaceRegexs, state.mActive);
         }
 
         private void assertNoErroredIfaces(final TetherState state, final String[] ifaceRegexs) {
             if (state == null || state.mErrored == null) return;
 
-            if (isIfaceMatch(ifaceRegexs, state.mErrored)) {
+            if (isAnyIfaceMatch(ifaceRegexs, state.mErrored)) {
                 fail("Found failed tethering interfaces: " + Arrays.toString(state.mErrored.toArray()));
             }
         }
@@ -256,12 +256,13 @@
 
         try {
             tetherEventCallback.assumeWifiTetheringSupported(mContext);
+            tetherEventCallback.expectNoTetheringActive();
 
-            mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+            final TetheringInterface tetheredIface =
+                    mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
 
-            final List<String> tetheredIfaces = tetherEventCallback.getTetheredInterfaces();
-            assertEquals(1, tetheredIfaces.size());
-            final String wifiTetheringIface = tetheredIfaces.get(0);
+            assertNotNull(tetheredIface);
+            final String wifiTetheringIface = tetheredIface.getInterface();
 
             mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
 
@@ -272,7 +273,8 @@
                 if (ret == TETHER_ERROR_NO_ERROR) {
                     // If calling #tether successful, there is a callback to tell the result of
                     // tethering setup.
-                    tetherEventCallback.expectErrorOrTethered(wifiTetheringIface);
+                    tetherEventCallback.expectErrorOrTethered(
+                            new TetheringInterface(TETHERING_WIFI, wifiTetheringIface));
                 }
             } finally {
                 mTM.untether(wifiTetheringIface);
@@ -319,7 +321,7 @@
             mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
 
             mTM.stopAllTethering();
-            tetherEventCallback.expectTetheredInterfacesChanged(null);
+            tetherEventCallback.expectNoTetheringActive();
         } finally {
             mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
         }
@@ -417,6 +419,7 @@
 
         try {
             tetherEventCallback.assumeWifiTetheringSupported(mContext);
+            tetherEventCallback.expectNoTetheringActive();
 
             previousWifiEnabledState = mWm.isWifiEnabled();
             if (previousWifiEnabledState) {
diff --git a/tests/deflake/Android.bp b/tests/deflake/Android.bp
new file mode 100644
index 0000000..806f805
--- /dev/null
+++ b/tests/deflake/Android.bp
@@ -0,0 +1,35 @@
+//
+// 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 {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "FrameworksNetDeflakeTest",
+    srcs: ["src/**/*.kt"],
+    libs: [
+        "junit",
+        "tradefed",
+    ],
+    static_libs: [
+        "kotlin-test",
+        "net-host-tests-utils",
+    ],
+    data: [":FrameworksNetTests"],
+    test_suites: ["device-tests"],
+}
diff --git a/tests/deflake/src/com/android/server/net/FrameworksNetDeflakeTest.kt b/tests/deflake/src/com/android/server/net/FrameworksNetDeflakeTest.kt
new file mode 100644
index 0000000..6285525
--- /dev/null
+++ b/tests/deflake/src/com/android/server/net/FrameworksNetDeflakeTest.kt
@@ -0,0 +1,28 @@
+/*
+ * 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 com.android.server.net
+
+import com.android.testutils.host.DeflakeHostTestBase
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
+import org.junit.runner.RunWith
+
+@RunWith(DeviceJUnit4ClassRunner::class)
+class FrameworksNetDeflakeTest: DeflakeHostTestBase() {
+    override val runCount = 20
+    override val testApkFilename = "FrameworksNetTests.apk"
+    override val testClasses = listOf("com.android.server.ConnectivityServiceTest")
+}
\ No newline at end of file
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
new file mode 100644
index 0000000..5fe478f
--- /dev/null
+++ b/tests/integration/Android.bp
@@ -0,0 +1,72 @@
+//
+// 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 {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "FrameworksNetIntegrationTests",
+    platform_apis: true,
+    certificate: "platform",
+    srcs: [
+        "src/**/*.kt",
+        "src/**/*.aidl",
+    ],
+    libs: [
+        "android.test.mock",
+    ],
+    static_libs: [
+        "NetworkStackApiStableLib",
+        "androidx.test.ext.junit",
+        "frameworks-net-integration-testutils",
+        "kotlin-reflect",
+        "mockito-target-extended-minus-junit4",
+        "net-tests-utils",
+        "service-connectivity",
+        "services.core",
+        "services.net",
+        "testables",
+    ],
+    test_suites: ["device-tests"],
+    use_embedded_native_libs: true,
+    jni_libs: [
+        // For mockito extended
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+        // android_library does not include JNI libs: include NetworkStack dependencies here
+        "libnativehelper_compat_libc++",
+        "libnetworkstackutilsjni",
+    ],
+}
+
+// Utilities for testing framework code both in integration and unit tests.
+java_library {
+    name: "frameworks-net-integration-testutils",
+    srcs: ["util/**/*.java", "util/**/*.kt"],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.test.rules",
+        "junit",
+        "net-tests-utils",
+    ],
+    libs: [
+        "service-connectivity",
+        "services.core",
+        "services.net",
+    ],
+}
diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml
new file mode 100644
index 0000000..2e13689
--- /dev/null
+++ b/tests/integration/AndroidManifest.xml
@@ -0,0 +1,73 @@
+<?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="com.android.server.net.integrationtests">
+
+    <!-- For ConnectivityService registerReceiverAsUser (receiving broadcasts) -->
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
+    <!-- PermissionMonitor sets network permissions for each user -->
+    <uses-permission android:name="android.permission.MANAGE_USERS"/>
+    <!-- ConnectivityService sends notifications to BatteryStats -->
+    <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS"/>
+    <!-- Reading network status -->
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.NETWORK_FACTORY"/>
+    <!-- Obtain LinkProperties callbacks with sensitive fields -->
+    <uses-permission android:name="android.permission.NETWORK_SETTINGS" />
+    <uses-permission android:name="android.permission.NETWORK_STACK"/>
+    <uses-permission android:name="android.permission.OBSERVE_NETWORK_POLICY"/>
+    <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
+    <!-- Reading DeviceConfig flags -->
+    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
+    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
+    <!-- Querying the resources package -->
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner"/>
+
+        <!-- This manifest is merged with the base manifest of the real NetworkStack app.
+                         Remove the NetworkStackService from the base (real) manifest, and replace with a test
+                         service that responds to the same intent -->
+        <service android:name=".TestNetworkStackService"
+             android:process="com.android.server.net.integrationtests.testnetworkstack"
+             android:exported="true">
+            <intent-filter>
+                <action android:name="android.net.INetworkStackConnector.Test"/>
+            </intent-filter>
+        </service>
+        <service android:name=".NetworkStackInstrumentationService"
+             android:process="com.android.server.net.integrationtests.testnetworkstack"
+             android:exported="true">
+            <intent-filter>
+                <action android:name=".INetworkStackInstrumentation"/>
+            </intent-filter>
+        </service>
+        <service android:name="com.android.server.connectivity.ipmemorystore.RegularMaintenanceJobService"
+             android:process="com.android.server.net.integrationtests.testnetworkstack"
+             android:permission="android.permission.BIND_JOB_SERVICE"/>
+
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.server.net.integrationtests"
+         android:label="Frameworks Net Integration Tests"/>
+
+</manifest>
diff --git a/tests/integration/res/values/config.xml b/tests/integration/res/values/config.xml
new file mode 100644
index 0000000..2c8046f
--- /dev/null
+++ b/tests/integration/res/values/config.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!--
+    Override configuration for testing. The below settings use the config_ variants, which are
+    normally used by RROs to override the setting with highest priority. -->
+    <integer name="config_captive_portal_dns_probe_timeout">12500</integer>
+    <string name="config_captive_portal_http_url" translatable="false">http://test.android.com</string>
+    <string name="config_captive_portal_https_url" translatable="false">https://secure.test.android.com</string>
+    <string-array name="config_captive_portal_fallback_urls" translatable="false">
+        <item>http://fallback1.android.com</item>
+        <item>http://fallback2.android.com</item>
+    </string-array>
+    <string-array name="config_captive_portal_fallback_probe_specs" translatable="false">
+    </string-array>
+</resources>
diff --git a/tests/integration/src/android/net/TestNetworkStackClient.kt b/tests/integration/src/android/net/TestNetworkStackClient.kt
new file mode 100644
index 0000000..61ef5bd
--- /dev/null
+++ b/tests/integration/src/android/net/TestNetworkStackClient.kt
@@ -0,0 +1,75 @@
+/*
+ * 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
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.net.networkstack.NetworkStackClientBase
+import android.os.IBinder
+import com.android.server.net.integrationtests.TestNetworkStackService
+import org.mockito.Mockito.any
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import kotlin.test.fail
+
+const val TEST_ACTION_SUFFIX = ".Test"
+
+class TestNetworkStackClient(private val context: Context) : NetworkStackClientBase() {
+    // TODO: consider switching to TrackRecord for more expressive checks
+    private val lastCallbacks = HashMap<Network, INetworkMonitorCallbacks>()
+    private val moduleConnector = ConnectivityModuleConnector { _, action, _, _ ->
+        val intent = Intent(action)
+        val serviceName = TestNetworkStackService::class.qualifiedName
+                ?: fail("TestNetworkStackService name not found")
+        intent.component = ComponentName(context.packageName, serviceName)
+        return@ConnectivityModuleConnector intent
+    }.also { it.init(context) }
+
+    fun start() {
+        moduleConnector.startModuleService(
+                INetworkStackConnector::class.qualifiedName + TEST_ACTION_SUFFIX,
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) { connector ->
+            onNetworkStackConnected(INetworkStackConnector.Stub.asInterface(connector))
+        }
+    }
+
+    // base may be an instance of an inaccessible subclass, so non-spyable.
+    // Use a known open class that delegates to the original instance for all methods except
+    // asBinder. asBinder needs to use its own non-delegated implementation as otherwise it would
+    // return a binder token to a class that is not spied on.
+    open class NetworkMonitorCallbacksWrapper(private val base: INetworkMonitorCallbacks) :
+            INetworkMonitorCallbacks.Stub(), INetworkMonitorCallbacks by base {
+        // asBinder is implemented by both base class and delegate: specify explicitly
+        override fun asBinder(): IBinder {
+            return super.asBinder()
+        }
+    }
+
+    override fun makeNetworkMonitor(network: Network, name: String?, cb: INetworkMonitorCallbacks) {
+        val cbSpy = spy(NetworkMonitorCallbacksWrapper(cb))
+        lastCallbacks[network] = cbSpy
+        super.makeNetworkMonitor(network, name, cbSpy)
+    }
+
+    fun verifyNetworkMonitorCreated(network: Network, timeoutMs: Long) {
+        val cb = lastCallbacks[network]
+                ?: fail("NetworkMonitor for network $network not requested")
+        verify(cb, timeout(timeoutMs)).onNetworkMonitorCreated(any())
+    }
+}
\ No newline at end of file
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
new file mode 100644
index 0000000..e039ef0
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -0,0 +1,257 @@
+/*
+ * 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 com.android.server.net.integrationtests
+
+import android.app.usage.NetworkStatsManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Context.BIND_AUTO_CREATE
+import android.content.Context.BIND_IMPORTANT
+import android.content.Intent
+import android.content.ServiceConnection
+import android.net.ConnectivityManager
+import android.net.IDnsResolver
+import android.net.INetd
+import android.net.LinkProperties
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkRequest
+import android.net.TestNetworkStackClient
+import android.net.Uri
+import android.net.metrics.IpConnectivityLog
+import android.os.ConditionVariable
+import android.os.IBinder
+import android.os.SystemConfigManager
+import android.os.UserHandle
+import android.testing.TestableContext
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.ConnectivityService
+import com.android.server.NetworkAgentWrapper
+import com.android.server.TestNetIdManager
+import com.android.server.connectivity.MockableSystemProperties
+import com.android.server.connectivity.ProxyTracker
+import com.android.testutils.TestableNetworkCallback
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.AdditionalAnswers
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.MockitoAnnotations
+import org.mockito.Spy
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+const val SERVICE_BIND_TIMEOUT_MS = 5_000L
+const val TEST_TIMEOUT_MS = 10_000L
+
+/**
+ * Test that exercises an instrumented version of ConnectivityService against an instrumented
+ * NetworkStack in a different test process.
+ */
+@RunWith(AndroidJUnit4::class)
+class ConnectivityServiceIntegrationTest {
+    // lateinit used here for mocks as they need to be reinitialized between each test and the test
+    // should crash if they are used before being initialized.
+    @Mock
+    private lateinit var statsManager: NetworkStatsManager
+    @Mock
+    private lateinit var log: IpConnectivityLog
+    @Mock
+    private lateinit var netd: INetd
+    @Mock
+    private lateinit var dnsResolver: IDnsResolver
+    @Mock
+    private lateinit var systemConfigManager: SystemConfigManager
+    @Spy
+    private var context = TestableContext(realContext)
+
+    // lateinit for these three classes under test, as they should be reset to a different instance
+    // for every test but should always be initialized before use (or the test should crash).
+    private lateinit var networkStackClient: TestNetworkStackClient
+    private lateinit var service: ConnectivityService
+    private lateinit var cm: ConnectivityManager
+
+    companion object {
+        // lateinit for this binder token, as it must be initialized before any test code is run
+        // and use of it before init should crash the test.
+        private lateinit var nsInstrumentation: INetworkStackInstrumentation
+        private val bindingCondition = ConditionVariable(false)
+
+        private val realContext get() = InstrumentationRegistry.getInstrumentation().context
+        private val httpProbeUrl get() =
+            realContext.getResources().getString(R.string.config_captive_portal_http_url)
+        private val httpsProbeUrl get() =
+            realContext.getResources().getString(R.string.config_captive_portal_https_url)
+
+        private class InstrumentationServiceConnection : ServiceConnection {
+            override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+                Log.i("TestNetworkStack", "Service connected")
+                try {
+                    if (service == null) fail("Error binding to NetworkStack instrumentation")
+                    if (::nsInstrumentation.isInitialized) fail("Service already connected")
+                    nsInstrumentation = INetworkStackInstrumentation.Stub.asInterface(service)
+                } finally {
+                    bindingCondition.open()
+                }
+            }
+
+            override fun onServiceDisconnected(name: ComponentName?) = Unit
+        }
+
+        @BeforeClass
+        @JvmStatic
+        fun setUpClass() {
+            val intent = Intent(realContext, NetworkStackInstrumentationService::class.java)
+            intent.action = INetworkStackInstrumentation::class.qualifiedName
+            assertTrue(realContext.bindService(intent, InstrumentationServiceConnection(),
+                    BIND_AUTO_CREATE or BIND_IMPORTANT),
+                    "Error binding to instrumentation service")
+            assertTrue(bindingCondition.block(SERVICE_BIND_TIMEOUT_MS),
+                    "Timed out binding to instrumentation service " +
+                            "after $SERVICE_BIND_TIMEOUT_MS ms")
+        }
+    }
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        val asUserCtx = mock(Context::class.java, AdditionalAnswers.delegatesTo<Context>(context))
+        doReturn(UserHandle.ALL).`when`(asUserCtx).user
+        doReturn(asUserCtx).`when`(context).createContextAsUser(eq(UserHandle.ALL), anyInt())
+        doNothing().`when`(context).sendStickyBroadcast(any(), any())
+        doReturn(Context.SYSTEM_CONFIG_SERVICE).`when`(context)
+                .getSystemServiceName(SystemConfigManager::class.java)
+        doReturn(systemConfigManager).`when`(context)
+                .getSystemService(Context.SYSTEM_CONFIG_SERVICE)
+        doReturn(IntArray(0)).`when`(systemConfigManager).getSystemPermissionUids(anyString())
+
+        networkStackClient = TestNetworkStackClient(realContext)
+        networkStackClient.start()
+
+        service = TestConnectivityService(makeDependencies())
+        cm = ConnectivityManager(context, service)
+        context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm)
+        context.addMockSystemService(Context.NETWORK_STATS_SERVICE, statsManager)
+
+        service.systemReadyInternal()
+    }
+
+    private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService(
+            context, dnsResolver, log, netd, deps)
+
+    private fun makeDependencies(): ConnectivityService.Dependencies {
+        val deps = spy(ConnectivityService.Dependencies())
+        doReturn(networkStackClient).`when`(deps).networkStack
+        doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any())
+        doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties
+        doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager()
+        return deps
+    }
+
+    @After
+    fun tearDown() {
+        nsInstrumentation.clearAllState()
+    }
+
+    @Test
+    fun testValidation() {
+        val request = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build()
+        val testCallback = TestableNetworkCallback()
+
+        cm.registerNetworkCallback(request, testCallback)
+        nsInstrumentation.addHttpResponse(HttpResponse(httpProbeUrl, responseCode = 204))
+        nsInstrumentation.addHttpResponse(HttpResponse(httpsProbeUrl, responseCode = 204))
+
+        val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, LinkProperties(), null /* ncTemplate */,
+                context)
+        networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
+
+        na.addCapability(NET_CAPABILITY_INTERNET)
+        na.connect()
+
+        testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
+        assertEquals(2, nsInstrumentation.getRequestUrls().size)
+    }
+
+    @Test
+    fun testCapportApi() {
+        val request = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build()
+        val testCb = TestableNetworkCallback()
+        val apiUrl = "https://capport.android.com"
+
+        cm.registerNetworkCallback(request, testCb)
+        nsInstrumentation.addHttpResponse(HttpResponse(
+                apiUrl,
+                """
+                    |{
+                    |  "captive": true,
+                    |  "user-portal-url": "https://login.capport.android.com",
+                    |  "venue-info-url": "https://venueinfo.capport.android.com"
+                    |}
+                """.trimMargin()))
+
+        // Tests will fail if a non-mocked query is received: mock the HTTPS probe, but not the
+        // HTTP probe as it should not be sent.
+        // Even if the HTTPS probe succeeds, a portal should be detected as the API takes precedence
+        // in that case.
+        nsInstrumentation.addHttpResponse(HttpResponse(httpsProbeUrl, responseCode = 204))
+
+        val lp = LinkProperties()
+        lp.captivePortalApiUrl = Uri.parse(apiUrl)
+        val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, lp, null /* ncTemplate */, context)
+        networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
+
+        na.addCapability(NET_CAPABILITY_INTERNET)
+        na.connect()
+
+        testCb.expectAvailableCallbacks(na.network, validated = false, tmt = TEST_TIMEOUT_MS)
+
+        val capportData = testCb.expectLinkPropertiesThat(na, TEST_TIMEOUT_MS) {
+            it.captivePortalData != null
+        }.lp.captivePortalData
+        assertNotNull(capportData)
+        assertTrue(capportData.isCaptive)
+        assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl)
+        assertEquals(Uri.parse("https://venueinfo.capport.android.com"), capportData.venueInfoUrl)
+
+        val nc = testCb.expectCapabilitiesWith(NET_CAPABILITY_CAPTIVE_PORTAL, na, TEST_TIMEOUT_MS)
+        assertFalse(nc.hasCapability(NET_CAPABILITY_VALIDATED))
+    }
+}
\ No newline at end of file
diff --git a/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.aidl b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.aidl
new file mode 100644
index 0000000..9a2bcfe
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 com.android.server.net.integrationtests;
+
+parcelable HttpResponse;
\ No newline at end of file
diff --git a/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt
new file mode 100644
index 0000000..e206313
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt
@@ -0,0 +1,49 @@
+/*
+ * 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 com.android.server.net.integrationtests
+
+import android.os.Parcel
+import android.os.Parcelable
+
+data class HttpResponse(
+    val requestUrl: String,
+    val responseCode: Int,
+    val content: String = "",
+    val redirectUrl: String? = null
+) : Parcelable {
+    constructor(p: Parcel): this(p.readString(), p.readInt(), p.readString(), p.readString())
+    constructor(requestUrl: String, contentBody: String): this(
+            requestUrl,
+            responseCode = 200,
+            content = contentBody,
+            redirectUrl = null)
+
+    override fun writeToParcel(dest: Parcel, flags: Int) {
+        with(dest) {
+            writeString(requestUrl)
+            writeInt(responseCode)
+            writeString(content)
+            writeString(redirectUrl)
+        }
+    }
+
+    override fun describeContents() = 0
+    companion object CREATOR : Parcelable.Creator<HttpResponse> {
+        override fun createFromParcel(source: Parcel) = HttpResponse(source)
+        override fun newArray(size: Int) = arrayOfNulls<HttpResponse?>(size)
+    }
+}
\ No newline at end of file
diff --git a/tests/integration/src/com/android/server/net/integrationtests/INetworkStackInstrumentation.aidl b/tests/integration/src/com/android/server/net/integrationtests/INetworkStackInstrumentation.aidl
new file mode 100644
index 0000000..efc58ad
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/INetworkStackInstrumentation.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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 com.android.server.net.integrationtests;
+
+import com.android.server.net.integrationtests.HttpResponse;
+
+interface INetworkStackInstrumentation {
+    void clearAllState();
+    void addHttpResponse(in HttpResponse response);
+    List<String> getRequestUrls();
+}
\ No newline at end of file
diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
new file mode 100644
index 0000000..e807952
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
@@ -0,0 +1,84 @@
+/*
+ * 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 com.android.server.net.integrationtests
+
+import android.app.Service
+import android.content.Intent
+import java.net.URL
+import java.util.Collections
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentLinkedQueue
+import kotlin.collections.ArrayList
+import kotlin.test.fail
+
+/**
+ * An instrumentation interface for the NetworkStack that allows controlling behavior to
+ * facilitate integration tests.
+ */
+class NetworkStackInstrumentationService : Service() {
+    override fun onBind(intent: Intent) = InstrumentationConnector.asBinder()
+
+    object InstrumentationConnector : INetworkStackInstrumentation.Stub() {
+        private val httpResponses = ConcurrentHashMap<String, ConcurrentLinkedQueue<HttpResponse>>()
+                .run {
+                    withDefault { key -> getOrPut(key) { ConcurrentLinkedQueue() } }
+                }
+        private val httpRequestUrls = Collections.synchronizedList(ArrayList<String>())
+
+        /**
+         * Called when an HTTP request is being processed by NetworkMonitor. Returns the response
+         * that should be simulated.
+         */
+        fun processRequest(url: URL): HttpResponse {
+            val strUrl = url.toString()
+            httpRequestUrls.add(strUrl)
+            return httpResponses[strUrl]?.poll()
+                    ?: fail("No mocked response for request: $strUrl. " +
+                            "Mocked URL keys are: ${httpResponses.keys}")
+        }
+
+        /**
+         * Clear all state of this connector. This is intended for use between two tests, so all
+         * state should be reset as if the connector was just created.
+         */
+        override fun clearAllState() {
+            httpResponses.clear()
+            httpRequestUrls.clear()
+        }
+
+        /**
+         * Add a response to a future HTTP request.
+         *
+         * <p>For any subsequent HTTP/HTTPS query, the first response with a matching URL will be
+         * used to mock the query response.
+         *
+         * <p>All requests that are expected to be sent must have a mock response: if an unexpected
+         * request is seen, the test will fail.
+         */
+        override fun addHttpResponse(response: HttpResponse) {
+            httpResponses.getValue(response.requestUrl).add(response)
+        }
+
+        /**
+         * Get the ordered list of request URLs that have been sent by NetworkMonitor, and were
+         * answered based on mock responses.
+         */
+        override fun getRequestUrls(): List<String> {
+            return ArrayList(httpRequestUrls)
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
new file mode 100644
index 0000000..eff6658
--- /dev/null
+++ b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
@@ -0,0 +1,100 @@
+/*
+ * 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 com.android.server.net.integrationtests
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.net.INetworkMonitorCallbacks
+import android.net.Network
+import android.net.metrics.IpConnectivityLog
+import android.net.util.SharedLog
+import android.os.IBinder
+import com.android.networkstack.netlink.TcpSocketTracker
+import com.android.server.NetworkStackService
+import com.android.server.NetworkStackService.NetworkMonitorConnector
+import com.android.server.NetworkStackService.NetworkStackConnector
+import com.android.server.connectivity.NetworkMonitor
+import com.android.server.net.integrationtests.NetworkStackInstrumentationService.InstrumentationConnector
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import java.io.ByteArrayInputStream
+import java.net.HttpURLConnection
+import java.net.URL
+import java.net.URLConnection
+import java.nio.charset.StandardCharsets
+
+private const val TEST_NETID = 42
+
+/**
+ * Android service that can return an [android.net.INetworkStackConnector] which can be instrumented
+ * through [NetworkStackInstrumentationService].
+ * Useful in tests to create test instrumented NetworkStack components that can receive
+ * instrumentation commands through [NetworkStackInstrumentationService].
+ */
+class TestNetworkStackService : Service() {
+    override fun onBind(intent: Intent): IBinder = TestNetworkStackConnector(makeTestContext())
+
+    private fun makeTestContext() = spy(applicationContext).also {
+        doReturn(mock(IBinder::class.java)).`when`(it).getSystemService(Context.NETD_SERVICE)
+    }
+
+    private class TestPermissionChecker : NetworkStackService.PermissionChecker() {
+        override fun enforceNetworkStackCallingPermission() = Unit
+    }
+
+    private class NetworkMonitorDeps(private val privateDnsBypassNetwork: Network) :
+            NetworkMonitor.Dependencies() {
+        override fun getPrivateDnsBypassNetwork(network: Network?) = privateDnsBypassNetwork
+    }
+
+    private inner class TestNetworkStackConnector(context: Context) : NetworkStackConnector(
+            context, TestPermissionChecker(), NetworkStackService.Dependencies()) {
+
+        private val network = Network(TEST_NETID)
+        private val privateDnsBypassNetwork = TestNetwork(TEST_NETID)
+
+        private inner class TestNetwork(netId: Int) : Network(netId) {
+            override fun openConnection(url: URL): URLConnection {
+                val response = InstrumentationConnector.processRequest(url)
+                val responseBytes = response.content.toByteArray(StandardCharsets.UTF_8)
+
+                val connection = mock(HttpURLConnection::class.java)
+                doReturn(response.responseCode).`when`(connection).responseCode
+                doReturn(responseBytes.size.toLong()).`when`(connection).contentLengthLong
+                doReturn(response.redirectUrl).`when`(connection).getHeaderField("location")
+                doReturn(ByteArrayInputStream(responseBytes)).`when`(connection).inputStream
+                return connection
+            }
+        }
+
+        override fun makeNetworkMonitor(
+            network: Network,
+            name: String?,
+            cb: INetworkMonitorCallbacks
+        ) {
+            val nm = NetworkMonitor(this@TestNetworkStackService, cb,
+                    this.network,
+                    mock(IpConnectivityLog::class.java), mock(SharedLog::class.java),
+                    mock(NetworkStackService.NetworkStackServiceManager::class.java),
+                    NetworkMonitorDeps(privateDnsBypassNetwork),
+                    mock(TcpSocketTracker::class.java))
+            cb.onNetworkMonitorCreated(NetworkMonitorConnector(nm, TestPermissionChecker()))
+        }
+    }
+}
diff --git a/tests/integration/util/com/android/server/ConnectivityServiceTestUtils.kt b/tests/integration/util/com/android/server/ConnectivityServiceTestUtils.kt
new file mode 100644
index 0000000..165fd37
--- /dev/null
+++ b/tests/integration/util/com/android/server/ConnectivityServiceTestUtils.kt
@@ -0,0 +1,43 @@
+/*
+ * 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
+ */
+
+@file:JvmName("ConnectivityServiceTestUtils")
+
+package com.android.server
+
+import android.net.ConnectivityManager.TYPE_BLUETOOTH
+import android.net.ConnectivityManager.TYPE_ETHERNET
+import android.net.ConnectivityManager.TYPE_MOBILE
+import android.net.ConnectivityManager.TYPE_NONE
+import android.net.ConnectivityManager.TYPE_TEST
+import android.net.ConnectivityManager.TYPE_VPN
+import android.net.ConnectivityManager.TYPE_WIFI
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+
+fun transportToLegacyType(transport: Int) = when (transport) {
+    TRANSPORT_BLUETOOTH -> TYPE_BLUETOOTH
+    TRANSPORT_CELLULAR -> TYPE_MOBILE
+    TRANSPORT_ETHERNET -> TYPE_ETHERNET
+    TRANSPORT_TEST -> TYPE_TEST
+    TRANSPORT_VPN -> TYPE_VPN
+    TRANSPORT_WIFI -> TYPE_WIFI
+    else -> TYPE_NONE
+}
\ No newline at end of file
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
new file mode 100644
index 0000000..17db179
--- /dev/null
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -0,0 +1,388 @@
+/*
+ * 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 com.android.server;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+
+import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
+
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.net.NetworkSpecifier;
+import android.net.QosFilter;
+import android.net.SocketKeepalive;
+import android.os.ConditionVariable;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+import android.util.Range;
+
+import com.android.net.module.util.ArrayTrackRecord;
+import com.android.testutils.HandlerUtils;
+import com.android.testutils.TestableNetworkCallback;
+
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class NetworkAgentWrapper implements TestableNetworkCallback.HasNetwork {
+    private final NetworkCapabilities mNetworkCapabilities;
+    private final HandlerThread mHandlerThread;
+    private final Context mContext;
+    private final String mLogTag;
+    private final NetworkAgentConfig mNetworkAgentConfig;
+
+    private final ConditionVariable mDisconnected = new ConditionVariable();
+    private final ConditionVariable mPreventReconnectReceived = new ConditionVariable();
+    private final AtomicBoolean mConnected = new AtomicBoolean(false);
+    private NetworkScore mScore;
+    private NetworkAgent mNetworkAgent;
+    private int mStartKeepaliveError = SocketKeepalive.ERROR_UNSUPPORTED;
+    private int mStopKeepaliveError = SocketKeepalive.NO_KEEPALIVE;
+    // Controls how test network agent is going to wait before responding to keepalive
+    // start/stop. Useful when simulate KeepaliveTracker is waiting for response from modem.
+    private long mKeepaliveResponseDelay = 0L;
+    private Integer mExpectedKeepaliveSlot = null;
+    private final ArrayTrackRecord<CallbackType>.ReadHead mCallbackHistory =
+            new ArrayTrackRecord<CallbackType>().newReadHead();
+
+    public NetworkAgentWrapper(int transport, LinkProperties linkProperties,
+            NetworkCapabilities ncTemplate, Context context) throws Exception {
+        final int type = transportToLegacyType(transport);
+        final String typeName = ConnectivityManager.getNetworkTypeName(type);
+        mNetworkCapabilities = (ncTemplate != null) ? ncTemplate : new NetworkCapabilities();
+        mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        mNetworkCapabilities.addTransportType(transport);
+        switch (transport) {
+            case TRANSPORT_ETHERNET:
+                mScore = new NetworkScore.Builder().setLegacyInt(70).build();
+                break;
+            case TRANSPORT_WIFI:
+                mScore = new NetworkScore.Builder().setLegacyInt(60).build();
+                break;
+            case TRANSPORT_CELLULAR:
+                mScore = new NetworkScore.Builder().setLegacyInt(50).build();
+                break;
+            case TRANSPORT_WIFI_AWARE:
+                mScore = new NetworkScore.Builder().setLegacyInt(20).build();
+                break;
+            case TRANSPORT_VPN:
+                mNetworkCapabilities.removeCapability(NET_CAPABILITY_NOT_VPN);
+                // VPNs deduce the SUSPENDED capability from their underlying networks and there
+                // is no public API to let VPN services set it.
+                mNetworkCapabilities.removeCapability(NET_CAPABILITY_NOT_SUSPENDED);
+                mScore = new NetworkScore.Builder().setLegacyInt(101).build();
+                break;
+            default:
+                throw new UnsupportedOperationException("unimplemented network type");
+        }
+        mContext = context;
+        mLogTag = "Mock-" + typeName;
+        mHandlerThread = new HandlerThread(mLogTag);
+        mHandlerThread.start();
+
+        // extraInfo is set to "" by default in NetworkAgentConfig.
+        final String extraInfo = (transport == TRANSPORT_CELLULAR) ? "internet.apn" : "";
+        mNetworkAgentConfig = new NetworkAgentConfig.Builder()
+                .setLegacyType(type)
+                .setLegacyTypeName(typeName)
+                .setLegacyExtraInfo(extraInfo)
+                .build();
+        mNetworkAgent = makeNetworkAgent(linkProperties, mNetworkAgentConfig);
+    }
+
+    protected InstrumentedNetworkAgent makeNetworkAgent(LinkProperties linkProperties,
+            final NetworkAgentConfig nac) throws Exception {
+        return new InstrumentedNetworkAgent(this, linkProperties, nac);
+    }
+
+    public static class InstrumentedNetworkAgent extends NetworkAgent {
+        private final NetworkAgentWrapper mWrapper;
+        private static final String PROVIDER_NAME = "InstrumentedNetworkAgentProvider";
+
+        public InstrumentedNetworkAgent(NetworkAgentWrapper wrapper, LinkProperties lp,
+                NetworkAgentConfig nac) {
+            super(wrapper.mContext, wrapper.mHandlerThread.getLooper(), wrapper.mLogTag,
+                    wrapper.mNetworkCapabilities, lp, wrapper.mScore, nac,
+                    new NetworkProvider(wrapper.mContext, wrapper.mHandlerThread.getLooper(),
+                            PROVIDER_NAME));
+            mWrapper = wrapper;
+            register();
+        }
+
+        @Override
+        public void unwanted() {
+            mWrapper.mDisconnected.open();
+        }
+
+        @Override
+        public void startSocketKeepalive(Message msg) {
+            int slot = msg.arg1;
+            if (mWrapper.mExpectedKeepaliveSlot != null) {
+                assertEquals((int) mWrapper.mExpectedKeepaliveSlot, slot);
+            }
+            mWrapper.mHandlerThread.getThreadHandler().postDelayed(
+                    () -> onSocketKeepaliveEvent(slot, mWrapper.mStartKeepaliveError),
+                    mWrapper.mKeepaliveResponseDelay);
+        }
+
+        @Override
+        public void stopSocketKeepalive(Message msg) {
+            final int slot = msg.arg1;
+            mWrapper.mHandlerThread.getThreadHandler().postDelayed(
+                    () -> onSocketKeepaliveEvent(slot, mWrapper.mStopKeepaliveError),
+                    mWrapper.mKeepaliveResponseDelay);
+        }
+
+        @Override
+        public void onQosCallbackRegistered(final int qosCallbackId,
+                final @NonNull QosFilter filter) {
+            Log.i(mWrapper.mLogTag, "onQosCallbackRegistered");
+            mWrapper.mCallbackHistory.add(
+                    new CallbackType.OnQosCallbackRegister(qosCallbackId, filter));
+        }
+
+        @Override
+        public void onQosCallbackUnregistered(final int qosCallbackId) {
+            Log.i(mWrapper.mLogTag, "onQosCallbackUnregistered");
+            mWrapper.mCallbackHistory.add(new CallbackType.OnQosCallbackUnregister(qosCallbackId));
+        }
+
+        @Override
+        protected void preventAutomaticReconnect() {
+            mWrapper.mPreventReconnectReceived.open();
+        }
+
+        @Override
+        protected void addKeepalivePacketFilter(Message msg) {
+            Log.i(mWrapper.mLogTag, "Add keepalive packet filter.");
+        }
+
+        @Override
+        protected void removeKeepalivePacketFilter(Message msg) {
+            Log.i(mWrapper.mLogTag, "Remove keepalive packet filter.");
+        }
+    }
+
+    public void setScore(@NonNull final NetworkScore score) {
+        mScore = score;
+        mNetworkAgent.sendNetworkScore(score);
+    }
+
+    public void adjustScore(int change) {
+        final int newLegacyScore = mScore.getLegacyInt() + change;
+        final NetworkScore.Builder builder = new NetworkScore.Builder()
+                .setLegacyInt(newLegacyScore);
+        if (mNetworkCapabilities.hasTransport(TRANSPORT_WIFI) && newLegacyScore < 50) {
+            builder.setExiting(true);
+        }
+        mScore = builder.build();
+        mNetworkAgent.sendNetworkScore(mScore);
+    }
+
+    public NetworkScore getScore() {
+        return mScore;
+    }
+
+    public void explicitlySelected(boolean explicitlySelected, boolean acceptUnvalidated) {
+        mNetworkAgent.explicitlySelected(explicitlySelected, acceptUnvalidated);
+    }
+
+    public void addCapability(int capability) {
+        mNetworkCapabilities.addCapability(capability);
+        mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+    }
+
+    public void removeCapability(int capability) {
+        mNetworkCapabilities.removeCapability(capability);
+        mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+    }
+
+    public void setUids(Set<Range<Integer>> uids) {
+        mNetworkCapabilities.setUids(uids);
+        mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+    }
+
+    public void setSignalStrength(int signalStrength) {
+        mNetworkCapabilities.setSignalStrength(signalStrength);
+        mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+    }
+
+    public void setNetworkSpecifier(NetworkSpecifier networkSpecifier) {
+        mNetworkCapabilities.setNetworkSpecifier(networkSpecifier);
+        mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+    }
+
+    public void setNetworkCapabilities(NetworkCapabilities nc, boolean sendToConnectivityService) {
+        mNetworkCapabilities.set(nc);
+        if (sendToConnectivityService) {
+            mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+        }
+    }
+
+    public void connect() {
+        if (!mConnected.compareAndSet(false /* expect */, true /* update */)) {
+            // compareAndSet returns false when the value couldn't be updated because it did not
+            // match the expected value.
+            fail("Test NetworkAgents can only be connected once");
+        }
+        mNetworkAgent.markConnected();
+    }
+
+    public void suspend() {
+        removeCapability(NET_CAPABILITY_NOT_SUSPENDED);
+    }
+
+    public void resume() {
+        addCapability(NET_CAPABILITY_NOT_SUSPENDED);
+    }
+
+    public void disconnect() {
+        mNetworkAgent.unregister();
+    }
+
+    @Override
+    public Network getNetwork() {
+        return mNetworkAgent.getNetwork();
+    }
+
+    public void expectPreventReconnectReceived(long timeoutMs) {
+        assertTrue(mPreventReconnectReceived.block(timeoutMs));
+    }
+
+    public void expectDisconnected(long timeoutMs) {
+        assertTrue(mDisconnected.block(timeoutMs));
+    }
+
+    public void sendLinkProperties(LinkProperties lp) {
+        mNetworkAgent.sendLinkProperties(lp);
+    }
+
+    public void setStartKeepaliveEvent(int reason) {
+        mStartKeepaliveError = reason;
+    }
+
+    public void setStopKeepaliveEvent(int reason) {
+        mStopKeepaliveError = reason;
+    }
+
+    public void setKeepaliveResponseDelay(long delay) {
+        mKeepaliveResponseDelay = delay;
+    }
+
+    public void setExpectedKeepaliveSlot(Integer slot) {
+        mExpectedKeepaliveSlot = slot;
+    }
+
+    public NetworkAgent getNetworkAgent() {
+        return mNetworkAgent;
+    }
+
+    public NetworkCapabilities getNetworkCapabilities() {
+        return mNetworkCapabilities;
+    }
+
+    public int getLegacyType() {
+        return mNetworkAgentConfig.getLegacyType();
+    }
+
+    public String getExtraInfo() {
+        return mNetworkAgentConfig.getLegacyExtraInfo();
+    }
+
+    public @NonNull ArrayTrackRecord<CallbackType>.ReadHead getCallbackHistory() {
+        return mCallbackHistory;
+    }
+
+    public void waitForIdle(long timeoutMs) {
+        HandlerUtils.waitForIdle(mHandlerThread, timeoutMs);
+    }
+
+    abstract static class CallbackType {
+        final int mQosCallbackId;
+
+        protected CallbackType(final int qosCallbackId) {
+            mQosCallbackId = qosCallbackId;
+        }
+
+        static class OnQosCallbackRegister extends CallbackType {
+            final QosFilter mFilter;
+            OnQosCallbackRegister(final int qosCallbackId, final QosFilter filter) {
+                super(qosCallbackId);
+                mFilter = filter;
+            }
+
+            @Override
+            public boolean equals(final Object o) {
+                if (this == o) return true;
+                if (o == null || getClass() != o.getClass()) return false;
+                final OnQosCallbackRegister that = (OnQosCallbackRegister) o;
+                return mQosCallbackId == that.mQosCallbackId
+                        && Objects.equals(mFilter, that.mFilter);
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mQosCallbackId, mFilter);
+            }
+        }
+
+        static class OnQosCallbackUnregister extends CallbackType {
+            OnQosCallbackUnregister(final int qosCallbackId) {
+                super(qosCallbackId);
+            }
+
+            @Override
+            public boolean equals(final Object o) {
+                if (this == o) return true;
+                if (o == null || getClass() != o.getClass()) return false;
+                final OnQosCallbackUnregister that = (OnQosCallbackUnregister) o;
+                return mQosCallbackId == that.mQosCallbackId;
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mQosCallbackId);
+            }
+        }
+    }
+
+    public boolean isBypassableVpn() {
+        return mNetworkAgentConfig.isBypassableVpn();
+    }
+}
diff --git a/tests/integration/util/com/android/server/TestNetIdManager.kt b/tests/integration/util/com/android/server/TestNetIdManager.kt
new file mode 100644
index 0000000..938a694
--- /dev/null
+++ b/tests/integration/util/com/android/server/TestNetIdManager.kt
@@ -0,0 +1,39 @@
+/*
+ * 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 com.android.server
+
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * A [NetIdManager] that generates ID starting from [NetIdManager.MAX_NET_ID] and decreasing, rather
+ * than starting from [NetIdManager.MIN_NET_ID] and increasing.
+ *
+ * Useful for testing ConnectivityService, to minimize the risk of test ConnectivityService netIDs
+ * overlapping with netIDs used by the real ConnectivityService on the device.
+ *
+ * IDs may still overlap if many networks have been used on the device (so the "real" netIDs
+ * are close to MAX_NET_ID), but this is typically not the case when running unit tests. Also, there
+ * is no way to fully solve the overlap issue without altering ID allocation in non-test code, as
+ * the real ConnectivityService could start using netIds that have been used by the test in the
+ * past.
+ */
+class TestNetIdManager : NetIdManager() {
+    private val nextId = AtomicInteger(MAX_NET_ID)
+    override fun reserveNetId() = nextId.decrementAndGet()
+    override fun releaseNetId(id: Int) = Unit
+    fun peekNextNetId() = nextId.get() - 1
+}
diff --git a/tests/smoketest/Android.bp b/tests/smoketest/Android.bp
new file mode 100644
index 0000000..8011540
--- /dev/null
+++ b/tests/smoketest/Android.bp
@@ -0,0 +1,27 @@
+// This test exists only because the jni_libs list for these tests is difficult to
+// maintain: the test itself only depends on libnetworkstatsfactorytestjni, but the test
+// fails to load that library unless *all* the dependencies of that library are explicitly
+// listed in jni_libs. This means that whenever any of the dependencies changes the test
+// starts failing and breaking presubmits in frameworks/base. We cannot easily put
+// FrameworksNetTests into global presubmit because they are at times flaky, but this
+// test is effectively empty beyond validating that the libraries load correctly, and
+// thus should be stable enough to put in global presubmit.
+//
+// TODO: remove this hack when there is a better solution for jni_libs that includes
+// dependent libraries.
+package {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "FrameworksNetSmokeTests",
+    defaults: ["FrameworksNetTests-jni-defaults"],
+    srcs: ["java/SmokeTest.java"],
+    test_suites: ["device-tests"],
+    static_libs: [
+        "androidx.test.rules",
+        "mockito-target-minus-junit4",
+        "services.core",
+    ],
+}
diff --git a/tests/smoketest/AndroidManifest.xml b/tests/smoketest/AndroidManifest.xml
new file mode 100644
index 0000000..f1b9feb
--- /dev/null
+++ b/tests/smoketest/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?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="com.android.frameworks.tests.net.smoketest">
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.frameworks.tests.net.smoketest"
+        android:label="Frameworks Networking Smoke Tests" />
+</manifest>
diff --git a/tests/smoketest/AndroidTest.xml b/tests/smoketest/AndroidTest.xml
new file mode 100644
index 0000000..ac366e4
--- /dev/null
+++ b/tests/smoketest/AndroidTest.xml
@@ -0,0 +1,28 @@
+<?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="Runs Frameworks Networking Smoke Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="FrameworksNetSmokeTests.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-tag" value="FrameworksNetSmokeTests" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.frameworks.tests.net.smoketest" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/tests/smoketest/java/SmokeTest.java b/tests/smoketest/java/SmokeTest.java
new file mode 100644
index 0000000..7d6655f
--- /dev/null
+++ b/tests/smoketest/java/SmokeTest.java
@@ -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 com.android.server.net;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class SmokeTest {
+
+    @Test
+    public void testLoadJni() {
+        System.loadLibrary("networkstatsfactorytestjni");
+        assertEquals(0, 0x00);
+    }
+}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
new file mode 100644
index 0000000..a7f57e8
--- /dev/null
+++ b/tests/unit/Android.bp
@@ -0,0 +1,98 @@
+//########################################################################
+// Build FrameworksNetTests package
+//########################################################################
+package {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+    name: "FrameworksNetTests-jni-defaults",
+    jni_libs: [
+        "ld-android",
+        "libbacktrace",
+        "libbase",
+        "libbinder",
+        "libbpf",
+        "libbpf_android",
+        "libc++",
+        "libcgrouprc",
+        "libcrypto",
+        "libcutils",
+        "libdl_android",
+        "libhidl-gen-utils",
+        "libhidlbase",
+        "libjsoncpp",
+        "liblog",
+        "liblzma",
+        "libnativehelper",
+        "libnetdbpf",
+        "libnetdutils",
+        "libnetworkstatsfactorytestjni",
+        "libpackagelistparser",
+        "libpcre2",
+        "libprocessgroup",
+        "libselinux",
+        "libtinyxml2",
+        "libui",
+        "libunwindstack",
+        "libutils",
+        "libutilscallstack",
+        "libvndksupport",
+        "libziparchive",
+        "libz",
+        "netd_aidl_interface-V5-cpp",
+    ],
+}
+
+android_library {
+    name: "FrameworksNetTestsLib",
+    min_sdk_version: "30",
+    defaults: [
+        "framework-connectivity-test-defaults",
+    ],
+    srcs: [
+        "java/**/*.java",
+        "java/**/*.kt",
+    ],
+    jarjar_rules: "jarjar-rules.txt",
+    static_libs: [
+        "androidx.test.rules",
+        "bouncycastle-repackaged-unbundled",
+        "core-tests-support",
+        "FrameworksNetCommonTests",
+        "frameworks-base-testutils",
+        "frameworks-net-integration-testutils",
+        "framework-protos",
+        "mockito-target-minus-junit4",
+        "net-tests-utils",
+        "platform-test-annotations",
+        "service-connectivity-pre-jarjar",
+        "services.core",
+        "services.net",
+    ],
+    libs: [
+        "android.net.ipsec.ike.stubs.module_lib",
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+        "ServiceConnectivityResources",
+    ],
+    visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
+}
+
+android_test {
+    name: "FrameworksNetTests",
+    min_sdk_version: "30",
+    defaults: [
+        "framework-connectivity-test-defaults",
+        "FrameworksNetTests-jni-defaults",
+    ],
+    test_suites: ["device-tests"],
+    static_libs: [
+        "FrameworksNetTestsLib",
+    ],
+    jni_libs: [
+        "libservice-connectivity",
+    ]
+}
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..4c60ccf
--- /dev/null
+++ b/tests/unit/AndroidManifest.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.frameworks.tests.net">
+
+    <uses-permission android:name="android.permission.READ_LOGS" />
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.BROADCAST_STICKY" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
+    <uses-permission android:name="android.permission.MANAGE_APP_TOKENS" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+    <uses-permission android:name="android.permission.REAL_GET_TASKS" />
+    <uses-permission android:name="android.permission.GET_DETAILED_TASKS" />
+    <uses-permission android:name="android.permission.MANAGE_NETWORK_POLICY" />
+    <uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />
+    <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.MANAGE_USERS" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+    <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" />
+    <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.PACKET_KEEPALIVE_OFFLOAD" />
+    <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT" />
+    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
+    <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
+    <uses-permission android:name="android.permission.NETWORK_STACK" />
+    <uses-permission android:name="android.permission.OBSERVE_NETWORK_POLICY" />
+    <uses-permission android:name="android.permission.NETWORK_FACTORY" />
+    <uses-permission android:name="android.permission.NETWORK_STATS_PROVIDER" />
+    <uses-permission android:name="android.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.net.ipsec.ike" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.frameworks.tests.net"
+        android:label="Frameworks Networking Tests" />
+</manifest>
diff --git a/tests/unit/AndroidTest.xml b/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..939ae49
--- /dev/null
+++ b/tests/unit/AndroidTest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs Frameworks Networking Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="FrameworksNetTests.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-tag" value="FrameworksNetTests" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.frameworks.tests.net" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/tests/unit/jarjar-rules.txt b/tests/unit/jarjar-rules.txt
new file mode 100644
index 0000000..ca88672
--- /dev/null
+++ b/tests/unit/jarjar-rules.txt
@@ -0,0 +1,2 @@
+# Module library in frameworks/libs/net
+rule com.android.net.module.util.** android.net.frameworktests.util.@1
diff --git a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
new file mode 100644
index 0000000..6bd2bd5
--- /dev/null
+++ b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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.app.usage;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.net.ConnectivityManager;
+import android.net.INetworkStatsService;
+import android.net.INetworkStatsSession;
+import android.net.NetworkStats.Entry;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.os.RemoteException;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkStatsManagerTest {
+    private static final String TEST_SUBSCRIBER_ID = "subid";
+
+    private @Mock INetworkStatsService mService;
+    private @Mock INetworkStatsSession mStatsSession;
+
+    private NetworkStatsManager mManager;
+
+    // TODO: change to NetworkTemplate.MATCH_MOBILE once internal constant rename is merged to aosp.
+    private static final int MATCH_MOBILE_ALL = 1;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mManager = new NetworkStatsManager(InstrumentationRegistry.getContext(), mService);
+    }
+
+    @Test
+    public void testQueryDetails() throws RemoteException {
+        final long startTime = 1;
+        final long endTime = 100;
+        final int uid1 = 10001;
+        final int uid2 = 10002;
+        final int uid3 = 10003;
+
+        Entry uid1Entry1 = new Entry("if1", uid1,
+                android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE,
+                100, 10, 200, 20, 0);
+
+        Entry uid1Entry2 = new Entry(
+                "if2", uid1,
+                android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE,
+                100, 10, 200, 20, 0);
+
+        Entry uid2Entry1 = new Entry("if1", uid2,
+                android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE,
+                150, 10, 250, 20, 0);
+
+        Entry uid2Entry2 = new Entry(
+                "if2", uid2,
+                android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE,
+                150, 10, 250, 20, 0);
+
+        NetworkStatsHistory history1 = new NetworkStatsHistory(10, 2);
+        history1.recordData(10, 20, uid1Entry1);
+        history1.recordData(20, 30, uid1Entry2);
+
+        NetworkStatsHistory history2 = new NetworkStatsHistory(10, 2);
+        history1.recordData(30, 40, uid2Entry1);
+        history1.recordData(35, 45, uid2Entry2);
+
+
+        when(mService.openSessionForUsageStats(anyInt(), anyString())).thenReturn(mStatsSession);
+        when(mStatsSession.getRelevantUids()).thenReturn(new int[] { uid1, uid2, uid3 });
+
+        when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class),
+                eq(uid1), eq(android.net.NetworkStats.SET_ALL),
+                eq(android.net.NetworkStats.TAG_NONE),
+                eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime)))
+                .then((InvocationOnMock inv) -> {
+                    NetworkTemplate template = inv.getArgument(0);
+                    assertEquals(MATCH_MOBILE_ALL, template.getMatchRule());
+                    assertEquals(TEST_SUBSCRIBER_ID, template.getSubscriberId());
+                    return history1;
+                });
+
+        when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class),
+                eq(uid2), eq(android.net.NetworkStats.SET_ALL),
+                eq(android.net.NetworkStats.TAG_NONE),
+                eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime)))
+                .then((InvocationOnMock inv) -> {
+                    NetworkTemplate template = inv.getArgument(0);
+                    assertEquals(MATCH_MOBILE_ALL, template.getMatchRule());
+                    assertEquals(TEST_SUBSCRIBER_ID, template.getSubscriberId());
+                    return history2;
+                });
+
+
+        NetworkStats stats = mManager.queryDetails(
+                ConnectivityManager.TYPE_MOBILE, TEST_SUBSCRIBER_ID, startTime, endTime);
+
+        NetworkStats.Bucket bucket = new NetworkStats.Bucket();
+
+        // First 2 buckets exactly match entry timings
+        assertTrue(stats.getNextBucket(bucket));
+        assertEquals(10, bucket.getStartTimeStamp());
+        assertEquals(20, bucket.getEndTimeStamp());
+        assertBucketMatches(uid1Entry1, bucket);
+
+        assertTrue(stats.getNextBucket(bucket));
+        assertEquals(20, bucket.getStartTimeStamp());
+        assertEquals(30, bucket.getEndTimeStamp());
+        assertBucketMatches(uid1Entry2, bucket);
+
+        // 30 -> 40: contains uid2Entry1 and half of uid2Entry2
+        assertTrue(stats.getNextBucket(bucket));
+        assertEquals(30, bucket.getStartTimeStamp());
+        assertEquals(40, bucket.getEndTimeStamp());
+        assertEquals(225, bucket.getRxBytes());
+        assertEquals(15, bucket.getRxPackets());
+        assertEquals(375, bucket.getTxBytes());
+        assertEquals(30, bucket.getTxPackets());
+
+        // 40 -> 50: contains half of uid2Entry2
+        assertTrue(stats.getNextBucket(bucket));
+        assertEquals(40, bucket.getStartTimeStamp());
+        assertEquals(50, bucket.getEndTimeStamp());
+        assertEquals(75, bucket.getRxBytes());
+        assertEquals(5, bucket.getRxPackets());
+        assertEquals(125, bucket.getTxBytes());
+        assertEquals(10, bucket.getTxPackets());
+
+        assertFalse(stats.hasNextBucket());
+    }
+
+    private void runQueryDetailsAndCheckTemplate(int networkType, String subscriberId,
+            NetworkTemplate expectedTemplate) throws RemoteException {
+        final long startTime = 1;
+        final long endTime = 100;
+        final int uid1 = 10001;
+        final int uid2 = 10002;
+
+        reset(mStatsSession);
+        when(mService.openSessionForUsageStats(anyInt(), anyString())).thenReturn(mStatsSession);
+        when(mStatsSession.getRelevantUids()).thenReturn(new int[] { uid1, uid2 });
+        when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class),
+                anyInt(), anyInt(), anyInt(), anyInt(), anyLong(), anyLong()))
+                .thenReturn(new NetworkStatsHistory(10, 0));
+        NetworkStats stats = mManager.queryDetails(
+                networkType, subscriberId, startTime, endTime);
+
+        verify(mStatsSession, times(1)).getHistoryIntervalForUid(
+                eq(expectedTemplate),
+                eq(uid1), eq(android.net.NetworkStats.SET_ALL),
+                eq(android.net.NetworkStats.TAG_NONE),
+                eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime));
+
+        verify(mStatsSession, times(1)).getHistoryIntervalForUid(
+                eq(expectedTemplate),
+                eq(uid2), eq(android.net.NetworkStats.SET_ALL),
+                eq(android.net.NetworkStats.TAG_NONE),
+                eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime));
+
+        assertFalse(stats.hasNextBucket());
+    }
+
+    @Test
+    public void testNetworkTemplateWhenRunningQueryDetails_NoSubscriberId() throws RemoteException {
+        runQueryDetailsAndCheckTemplate(ConnectivityManager.TYPE_MOBILE,
+                null /* subscriberId */, NetworkTemplate.buildTemplateMobileWildcard());
+        runQueryDetailsAndCheckTemplate(ConnectivityManager.TYPE_WIFI,
+                "" /* subscriberId */, NetworkTemplate.buildTemplateWifiWildcard());
+        runQueryDetailsAndCheckTemplate(ConnectivityManager.TYPE_WIFI,
+                null /* subscriberId */, NetworkTemplate.buildTemplateWifiWildcard());
+    }
+
+    @Test
+    public void testNetworkTemplateWhenRunningQueryDetails_MergedCarrierWifi()
+            throws RemoteException {
+        runQueryDetailsAndCheckTemplate(ConnectivityManager.TYPE_WIFI,
+                TEST_SUBSCRIBER_ID,
+                NetworkTemplate.buildTemplateWifi(NetworkTemplate.WIFI_NETWORKID_ALL,
+                        TEST_SUBSCRIBER_ID));
+    }
+
+    private void assertBucketMatches(Entry expected, NetworkStats.Bucket actual) {
+        assertEquals(expected.uid, actual.getUid());
+        assertEquals(expected.rxBytes, actual.getRxBytes());
+        assertEquals(expected.rxPackets, actual.getRxPackets());
+        assertEquals(expected.txBytes, actual.getTxBytes());
+        assertEquals(expected.txPackets, actual.getTxPackets());
+    }
+}
diff --git a/tests/unit/java/android/net/ConnectivityDiagnosticsManagerTest.java b/tests/unit/java/android/net/ConnectivityDiagnosticsManagerTest.java
new file mode 100644
index 0000000..06e9405
--- /dev/null
+++ b/tests/unit/java/android/net/ConnectivityDiagnosticsManagerTest.java
@@ -0,0 +1,384 @@
+/*
+ * 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;
+
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsBinder;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport;
+
+import static com.android.testutils.ParcelUtils.assertParcelSane;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.content.Context;
+import android.os.PersistableBundle;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+import java.util.concurrent.Executor;
+
+@RunWith(JUnit4.class)
+public class ConnectivityDiagnosticsManagerTest {
+    private static final int NET_ID = 1;
+    private static final int DETECTION_METHOD = 2;
+    private static final long TIMESTAMP = 10L;
+    private static final String INTERFACE_NAME = "interface";
+    private static final String BUNDLE_KEY = "key";
+    private static final String BUNDLE_VALUE = "value";
+
+    private static final Executor INLINE_EXECUTOR = x -> x.run();
+
+    @Mock private IConnectivityManager mService;
+    @Mock private ConnectivityDiagnosticsCallback mCb;
+
+    private Context mContext;
+    private ConnectivityDiagnosticsBinder mBinder;
+    private ConnectivityDiagnosticsManager mManager;
+
+    private String mPackageName;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getContext();
+
+        mService = mock(IConnectivityManager.class);
+        mCb = mock(ConnectivityDiagnosticsCallback.class);
+
+        mBinder = new ConnectivityDiagnosticsBinder(mCb, INLINE_EXECUTOR);
+        mManager = new ConnectivityDiagnosticsManager(mContext, mService);
+
+        mPackageName = mContext.getOpPackageName();
+    }
+
+    @After
+    public void tearDown() {
+        // clear ConnectivityDiagnosticsManager callbacks map
+        ConnectivityDiagnosticsManager.sCallbacks.clear();
+    }
+
+    private ConnectivityReport createSampleConnectivityReport() {
+        final LinkProperties linkProperties = new LinkProperties();
+        linkProperties.setInterfaceName(INTERFACE_NAME);
+
+        final NetworkCapabilities networkCapabilities = new NetworkCapabilities();
+        networkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_IMS);
+
+        final PersistableBundle bundle = new PersistableBundle();
+        bundle.putString(BUNDLE_KEY, BUNDLE_VALUE);
+
+        return new ConnectivityReport(
+                new Network(NET_ID), TIMESTAMP, linkProperties, networkCapabilities, bundle);
+    }
+
+    private ConnectivityReport createDefaultConnectivityReport() {
+        return new ConnectivityReport(
+                new Network(0),
+                0L,
+                new LinkProperties(),
+                new NetworkCapabilities(),
+                PersistableBundle.EMPTY);
+    }
+
+    @Test
+    public void testPersistableBundleEquals() {
+        assertFalse(
+                ConnectivityDiagnosticsManager.persistableBundleEquals(
+                        null, PersistableBundle.EMPTY));
+        assertFalse(
+                ConnectivityDiagnosticsManager.persistableBundleEquals(
+                        PersistableBundle.EMPTY, null));
+        assertTrue(
+                ConnectivityDiagnosticsManager.persistableBundleEquals(
+                        PersistableBundle.EMPTY, PersistableBundle.EMPTY));
+
+        final PersistableBundle a = new PersistableBundle();
+        a.putString(BUNDLE_KEY, BUNDLE_VALUE);
+
+        final PersistableBundle b = new PersistableBundle();
+        b.putString(BUNDLE_KEY, BUNDLE_VALUE);
+
+        final PersistableBundle c = new PersistableBundle();
+        c.putString(BUNDLE_KEY, null);
+
+        assertFalse(
+                ConnectivityDiagnosticsManager.persistableBundleEquals(PersistableBundle.EMPTY, a));
+        assertFalse(
+                ConnectivityDiagnosticsManager.persistableBundleEquals(a, PersistableBundle.EMPTY));
+
+        assertTrue(ConnectivityDiagnosticsManager.persistableBundleEquals(a, b));
+        assertTrue(ConnectivityDiagnosticsManager.persistableBundleEquals(b, a));
+
+        assertFalse(ConnectivityDiagnosticsManager.persistableBundleEquals(a, c));
+        assertFalse(ConnectivityDiagnosticsManager.persistableBundleEquals(c, a));
+    }
+
+    @Test
+    public void testConnectivityReportEquals() {
+        final ConnectivityReport defaultReport = createDefaultConnectivityReport();
+        final ConnectivityReport sampleReport = createSampleConnectivityReport();
+        assertEquals(sampleReport, createSampleConnectivityReport());
+        assertEquals(defaultReport, createDefaultConnectivityReport());
+
+        final LinkProperties linkProperties = sampleReport.getLinkProperties();
+        final NetworkCapabilities networkCapabilities = sampleReport.getNetworkCapabilities();
+        final PersistableBundle bundle = sampleReport.getAdditionalInfo();
+
+        assertNotEquals(
+                createDefaultConnectivityReport(),
+                new ConnectivityReport(
+                        new Network(NET_ID),
+                        0L,
+                        new LinkProperties(),
+                        new NetworkCapabilities(),
+                        PersistableBundle.EMPTY));
+        assertNotEquals(
+                createDefaultConnectivityReport(),
+                new ConnectivityReport(
+                        new Network(0),
+                        TIMESTAMP,
+                        new LinkProperties(),
+                        new NetworkCapabilities(),
+                        PersistableBundle.EMPTY));
+        assertNotEquals(
+                createDefaultConnectivityReport(),
+                new ConnectivityReport(
+                        new Network(0),
+                        0L,
+                        linkProperties,
+                        new NetworkCapabilities(),
+                        PersistableBundle.EMPTY));
+        assertNotEquals(
+                createDefaultConnectivityReport(),
+                new ConnectivityReport(
+                        new Network(0),
+                        TIMESTAMP,
+                        new LinkProperties(),
+                        networkCapabilities,
+                        PersistableBundle.EMPTY));
+        assertNotEquals(
+                createDefaultConnectivityReport(),
+                new ConnectivityReport(
+                        new Network(0),
+                        TIMESTAMP,
+                        new LinkProperties(),
+                        new NetworkCapabilities(),
+                        bundle));
+    }
+
+    @Test
+    public void testConnectivityReportParcelUnparcel() {
+        assertParcelSane(createSampleConnectivityReport(), 5);
+    }
+
+    private DataStallReport createSampleDataStallReport() {
+        final LinkProperties linkProperties = new LinkProperties();
+        linkProperties.setInterfaceName(INTERFACE_NAME);
+
+        final PersistableBundle bundle = new PersistableBundle();
+        bundle.putString(BUNDLE_KEY, BUNDLE_VALUE);
+
+        final NetworkCapabilities networkCapabilities = new NetworkCapabilities();
+        networkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_IMS);
+
+        return new DataStallReport(
+                new Network(NET_ID),
+                TIMESTAMP,
+                DETECTION_METHOD,
+                linkProperties,
+                networkCapabilities,
+                bundle);
+    }
+
+    private DataStallReport createDefaultDataStallReport() {
+        return new DataStallReport(
+                new Network(0),
+                0L,
+                0,
+                new LinkProperties(),
+                new NetworkCapabilities(),
+                PersistableBundle.EMPTY);
+    }
+
+    @Test
+    public void testDataStallReportEquals() {
+        final DataStallReport defaultReport = createDefaultDataStallReport();
+        final DataStallReport sampleReport = createSampleDataStallReport();
+        assertEquals(sampleReport, createSampleDataStallReport());
+        assertEquals(defaultReport, createDefaultDataStallReport());
+
+        final LinkProperties linkProperties = sampleReport.getLinkProperties();
+        final NetworkCapabilities networkCapabilities = sampleReport.getNetworkCapabilities();
+        final PersistableBundle bundle = sampleReport.getStallDetails();
+
+        assertNotEquals(
+                defaultReport,
+                new DataStallReport(
+                        new Network(NET_ID),
+                        0L,
+                        0,
+                        new LinkProperties(),
+                        new NetworkCapabilities(),
+                        PersistableBundle.EMPTY));
+        assertNotEquals(
+                defaultReport,
+                new DataStallReport(
+                        new Network(0),
+                        TIMESTAMP,
+                        0,
+                        new LinkProperties(),
+                        new NetworkCapabilities(),
+                        PersistableBundle.EMPTY));
+        assertNotEquals(
+                defaultReport,
+                new DataStallReport(
+                        new Network(0),
+                        0L,
+                        DETECTION_METHOD,
+                        new LinkProperties(),
+                        new NetworkCapabilities(),
+                        PersistableBundle.EMPTY));
+        assertNotEquals(
+                defaultReport,
+                new DataStallReport(
+                        new Network(0),
+                        0L,
+                        0,
+                        linkProperties,
+                        new NetworkCapabilities(),
+                        PersistableBundle.EMPTY));
+        assertNotEquals(
+                defaultReport,
+                new DataStallReport(
+                        new Network(0),
+                        0L,
+                        0,
+                        new LinkProperties(),
+                        networkCapabilities,
+                        PersistableBundle.EMPTY));
+        assertNotEquals(
+                defaultReport,
+                new DataStallReport(
+                        new Network(0),
+                        0L,
+                        0,
+                        new LinkProperties(),
+                        new NetworkCapabilities(),
+                        bundle));
+    }
+
+    @Test
+    public void testDataStallReportParcelUnparcel() {
+        assertParcelSane(createSampleDataStallReport(), 6);
+    }
+
+    @Test
+    public void testConnectivityDiagnosticsCallbackOnConnectivityReportAvailable() {
+        mBinder.onConnectivityReportAvailable(createSampleConnectivityReport());
+
+        // The callback will be invoked synchronously by inline executor. Immediately check the
+        // latch without waiting.
+        verify(mCb).onConnectivityReportAvailable(eq(createSampleConnectivityReport()));
+    }
+
+    @Test
+    public void testConnectivityDiagnosticsCallbackOnDataStallSuspected() {
+        mBinder.onDataStallSuspected(createSampleDataStallReport());
+
+        // The callback will be invoked synchronously by inline executor. Immediately check the
+        // latch without waiting.
+        verify(mCb).onDataStallSuspected(eq(createSampleDataStallReport()));
+    }
+
+    @Test
+    public void testConnectivityDiagnosticsCallbackOnNetworkConnectivityReported() {
+        final Network n = new Network(NET_ID);
+        final boolean connectivity = true;
+
+        mBinder.onNetworkConnectivityReported(n, connectivity);
+
+        // The callback will be invoked synchronously by inline executor. Immediately check the
+        // latch without waiting.
+        verify(mCb).onNetworkConnectivityReported(eq(n), eq(connectivity));
+    }
+
+    @Test
+    public void testRegisterConnectivityDiagnosticsCallback() throws Exception {
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+
+        mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb);
+
+        verify(mService).registerConnectivityDiagnosticsCallback(
+                any(ConnectivityDiagnosticsBinder.class), eq(request), eq(mPackageName));
+        assertTrue(ConnectivityDiagnosticsManager.sCallbacks.containsKey(mCb));
+    }
+
+    @Test
+    public void testRegisterDuplicateConnectivityDiagnosticsCallback() throws Exception {
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+
+        mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb);
+
+        try {
+            mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb);
+            fail("Duplicate callback registration should fail");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testUnregisterConnectivityDiagnosticsCallback() throws Exception {
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb);
+
+        mManager.unregisterConnectivityDiagnosticsCallback(mCb);
+
+        verify(mService).unregisterConnectivityDiagnosticsCallback(
+                any(ConnectivityDiagnosticsBinder.class));
+        assertFalse(ConnectivityDiagnosticsManager.sCallbacks.containsKey(mCb));
+
+        // verify that re-registering is successful
+        mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb);
+        verify(mService, times(2)).registerConnectivityDiagnosticsCallback(
+                any(ConnectivityDiagnosticsBinder.class), eq(request), eq(mPackageName));
+        assertTrue(ConnectivityDiagnosticsManager.sCallbacks.containsKey(mCb));
+    }
+
+    @Test
+    public void testUnregisterUnknownConnectivityDiagnosticsCallback() throws Exception {
+        mManager.unregisterConnectivityDiagnosticsCallback(mCb);
+
+        verifyNoMoreInteractions(mService);
+    }
+}
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
new file mode 100644
index 0000000..07f22a2
--- /dev/null
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -0,0 +1,433 @@
+/*
+ * 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.ConnectivityManager.TYPE_NONE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
+import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST;
+import static android.net.NetworkRequest.Type.REQUEST;
+import static android.net.NetworkRequest.Type.TRACK_DEFAULT;
+import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT;
+
+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 static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+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;
+import static org.mockito.Mockito.when;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Process;
+
+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.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConnectivityManagerTest {
+
+    @Mock Context mCtx;
+    @Mock IConnectivityManager mService;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    static NetworkCapabilities verifyNetworkCapabilities(
+            int legacyType, int transportType, int... capabilities) {
+        final NetworkCapabilities nc = ConnectivityManager.networkCapabilitiesForType(legacyType);
+        assertNotNull(nc);
+        assertTrue(nc.hasTransport(transportType));
+        for (int capability : capabilities) {
+            assertTrue(nc.hasCapability(capability));
+        }
+
+        return nc;
+    }
+
+    static void verifyUnrestrictedNetworkCapabilities(int legacyType, int transportType) {
+        verifyNetworkCapabilities(
+                legacyType,
+                transportType,
+                NET_CAPABILITY_INTERNET,
+                NET_CAPABILITY_NOT_RESTRICTED,
+                NET_CAPABILITY_NOT_VPN,
+                NET_CAPABILITY_TRUSTED);
+    }
+
+    static void verifyRestrictedMobileNetworkCapabilities(int legacyType, int capability) {
+        final NetworkCapabilities nc = verifyNetworkCapabilities(
+                legacyType,
+                TRANSPORT_CELLULAR,
+                capability,
+                NET_CAPABILITY_NOT_VPN,
+                NET_CAPABILITY_TRUSTED);
+
+        assertFalse(nc.hasCapability(NET_CAPABILITY_INTERNET));
+        assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+    }
+
+    @Test
+    public void testNetworkCapabilitiesForTypeMobile() {
+        verifyUnrestrictedNetworkCapabilities(
+                ConnectivityManager.TYPE_MOBILE, TRANSPORT_CELLULAR);
+    }
+
+    @Test
+    public void testNetworkCapabilitiesForTypeMobileCbs() {
+        verifyRestrictedMobileNetworkCapabilities(
+                ConnectivityManager.TYPE_MOBILE_CBS, NET_CAPABILITY_CBS);
+    }
+
+    @Test
+    public void testNetworkCapabilitiesForTypeMobileDun() {
+        verifyRestrictedMobileNetworkCapabilities(
+                ConnectivityManager.TYPE_MOBILE_DUN, NET_CAPABILITY_DUN);
+    }
+
+    @Test
+    public void testNetworkCapabilitiesForTypeMobileFota() {
+        verifyRestrictedMobileNetworkCapabilities(
+                ConnectivityManager.TYPE_MOBILE_FOTA, NET_CAPABILITY_FOTA);
+    }
+
+    @Test
+    public void testNetworkCapabilitiesForTypeMobileHipri() {
+        verifyUnrestrictedNetworkCapabilities(
+                ConnectivityManager.TYPE_MOBILE_HIPRI, TRANSPORT_CELLULAR);
+    }
+
+    @Test
+    public void testNetworkCapabilitiesForTypeMobileIms() {
+        verifyRestrictedMobileNetworkCapabilities(
+                ConnectivityManager.TYPE_MOBILE_IMS, NET_CAPABILITY_IMS);
+    }
+
+    @Test
+    public void testNetworkCapabilitiesForTypeMobileMms() {
+        final NetworkCapabilities nc = verifyNetworkCapabilities(
+                ConnectivityManager.TYPE_MOBILE_MMS,
+                TRANSPORT_CELLULAR,
+                NET_CAPABILITY_MMS,
+                NET_CAPABILITY_NOT_VPN,
+                NET_CAPABILITY_TRUSTED);
+
+        assertFalse(nc.hasCapability(NET_CAPABILITY_INTERNET));
+    }
+
+    @Test
+    public void testNetworkCapabilitiesForTypeMobileSupl() {
+        final NetworkCapabilities nc = verifyNetworkCapabilities(
+                ConnectivityManager.TYPE_MOBILE_SUPL,
+                TRANSPORT_CELLULAR,
+                NET_CAPABILITY_SUPL,
+                NET_CAPABILITY_NOT_VPN,
+                NET_CAPABILITY_TRUSTED);
+
+        assertFalse(nc.hasCapability(NET_CAPABILITY_INTERNET));
+    }
+
+    @Test
+    public void testNetworkCapabilitiesForTypeWifi() {
+        verifyUnrestrictedNetworkCapabilities(
+                ConnectivityManager.TYPE_WIFI, TRANSPORT_WIFI);
+    }
+
+    @Test
+    public void testNetworkCapabilitiesForTypeWifiP2p() {
+        final NetworkCapabilities nc = verifyNetworkCapabilities(
+                ConnectivityManager.TYPE_WIFI_P2P,
+                TRANSPORT_WIFI,
+                NET_CAPABILITY_NOT_RESTRICTED, NET_CAPABILITY_NOT_VPN,
+                NET_CAPABILITY_TRUSTED, NET_CAPABILITY_WIFI_P2P);
+
+        assertFalse(nc.hasCapability(NET_CAPABILITY_INTERNET));
+    }
+
+    @Test
+    public void testNetworkCapabilitiesForTypeBluetooth() {
+        verifyUnrestrictedNetworkCapabilities(
+                ConnectivityManager.TYPE_BLUETOOTH, TRANSPORT_BLUETOOTH);
+    }
+
+    @Test
+    public void testNetworkCapabilitiesForTypeEthernet() {
+        verifyUnrestrictedNetworkCapabilities(
+                ConnectivityManager.TYPE_ETHERNET, TRANSPORT_ETHERNET);
+    }
+
+    @Test
+    public void testCallbackRelease() throws Exception {
+        ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+        NetworkRequest request = makeRequest(1);
+        NetworkCallback callback = mock(ConnectivityManager.NetworkCallback.class,
+                CALLS_REAL_METHODS);
+        Handler handler = new Handler(Looper.getMainLooper());
+        ArgumentCaptor<Messenger> captor = ArgumentCaptor.forClass(Messenger.class);
+
+        // register callback
+        when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(),
+                anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(request);
+        manager.requestNetwork(request, callback, handler);
+
+        // callback triggers
+        captor.getValue().send(makeMessage(request, ConnectivityManager.CALLBACK_AVAILABLE));
+        verify(callback, timeout(500).times(1)).onAvailable(any(Network.class),
+                any(NetworkCapabilities.class), any(LinkProperties.class), anyBoolean());
+
+        // unregister callback
+        manager.unregisterNetworkCallback(callback);
+        verify(mService, times(1)).releaseNetworkRequest(request);
+
+        // callback does not trigger anymore.
+        captor.getValue().send(makeMessage(request, ConnectivityManager.CALLBACK_LOSING));
+        verify(callback, timeout(500).times(0)).onLosing(any(), anyInt());
+    }
+
+    @Test
+    public void testCallbackRecycling() throws Exception {
+        ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+        NetworkRequest req1 = makeRequest(1);
+        NetworkRequest req2 = makeRequest(2);
+        NetworkCallback callback = mock(ConnectivityManager.NetworkCallback.class,
+                CALLS_REAL_METHODS);
+        Handler handler = new Handler(Looper.getMainLooper());
+        ArgumentCaptor<Messenger> captor = ArgumentCaptor.forClass(Messenger.class);
+
+        // register callback
+        when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(),
+                anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(req1);
+        manager.requestNetwork(req1, callback, handler);
+
+        // callback triggers
+        captor.getValue().send(makeMessage(req1, ConnectivityManager.CALLBACK_AVAILABLE));
+        verify(callback, timeout(100).times(1)).onAvailable(any(Network.class),
+                any(NetworkCapabilities.class), any(LinkProperties.class), anyBoolean());
+
+        // unregister callback
+        manager.unregisterNetworkCallback(callback);
+        verify(mService, times(1)).releaseNetworkRequest(req1);
+
+        // callback does not trigger anymore.
+        captor.getValue().send(makeMessage(req1, ConnectivityManager.CALLBACK_LOSING));
+        verify(callback, timeout(100).times(0)).onLosing(any(), anyInt());
+
+        // callback can be registered again
+        when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(),
+                anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(req2);
+        manager.requestNetwork(req2, callback, handler);
+
+        // callback triggers
+        captor.getValue().send(makeMessage(req2, ConnectivityManager.CALLBACK_LOST));
+        verify(callback, timeout(100).times(1)).onLost(any());
+
+        // unregister callback
+        manager.unregisterNetworkCallback(callback);
+        verify(mService, times(1)).releaseNetworkRequest(req2);
+    }
+
+    // TODO: turn on this test when request  callback 1:1 mapping is enforced
+    //@Test
+    private void noDoubleCallbackRegistration() throws Exception {
+        ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+        NetworkRequest request = makeRequest(1);
+        NetworkCallback callback = new ConnectivityManager.NetworkCallback();
+        ApplicationInfo info = new ApplicationInfo();
+        // TODO: update version when starting to enforce 1:1 mapping
+        info.targetSdkVersion = VERSION_CODES.N_MR1 + 1;
+
+        when(mCtx.getApplicationInfo()).thenReturn(info);
+        when(mService.requestNetwork(anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(),
+                anyInt(), any(), nullable(String.class))).thenReturn(request);
+
+        Handler handler = new Handler(Looper.getMainLooper());
+        manager.requestNetwork(request, callback, handler);
+
+        // callback is already registered, reregistration should fail.
+        Class<IllegalArgumentException> wantException = IllegalArgumentException.class;
+        expectThrowable(() -> manager.requestNetwork(request, callback), wantException);
+
+        manager.unregisterNetworkCallback(callback);
+        verify(mService, times(1)).releaseNetworkRequest(request);
+
+        // unregistering the callback should make it registrable again.
+        manager.requestNetwork(request, callback);
+    }
+
+    @Test
+    public void testArgumentValidation() throws Exception {
+        ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+
+        NetworkRequest request = mock(NetworkRequest.class);
+        NetworkCallback callback = mock(NetworkCallback.class);
+        Handler handler = mock(Handler.class);
+        NetworkCallback nullCallback = null;
+        PendingIntent nullIntent = null;
+
+        mustFail(() -> { manager.requestNetwork(null, callback); });
+        mustFail(() -> { manager.requestNetwork(request, nullCallback); });
+        mustFail(() -> { manager.requestNetwork(request, callback, null); });
+        mustFail(() -> { manager.requestNetwork(request, callback, -1); });
+        mustFail(() -> { manager.requestNetwork(request, nullIntent); });
+
+        mustFail(() -> { manager.registerNetworkCallback(null, callback, handler); });
+        mustFail(() -> { manager.registerNetworkCallback(request, null, handler); });
+        mustFail(() -> { manager.registerNetworkCallback(request, callback, null); });
+        mustFail(() -> { manager.registerNetworkCallback(request, nullIntent); });
+
+        mustFail(() -> { manager.registerDefaultNetworkCallback(null, handler); });
+        mustFail(() -> { manager.registerDefaultNetworkCallback(callback, null); });
+
+        mustFail(() -> { manager.registerSystemDefaultNetworkCallback(null, handler); });
+        mustFail(() -> { manager.registerSystemDefaultNetworkCallback(callback, null); });
+
+        mustFail(() -> { manager.unregisterNetworkCallback(nullCallback); });
+        mustFail(() -> { manager.unregisterNetworkCallback(nullIntent); });
+        mustFail(() -> { manager.releaseNetworkRequest(nullIntent); });
+    }
+
+    static void mustFail(Runnable fn) {
+        try {
+            fn.run();
+            fail();
+        } catch (Exception expected) {
+        }
+    }
+
+    @Test
+    public void testRequestType() throws Exception {
+        final String testPkgName = "MyPackage";
+        final String testAttributionTag = "MyTag";
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+        when(mCtx.getOpPackageName()).thenReturn(testPkgName);
+        when(mCtx.getAttributionTag()).thenReturn(testAttributionTag);
+        final NetworkRequest request = makeRequest(1);
+        final NetworkCallback callback = new ConnectivityManager.NetworkCallback();
+
+        manager.requestNetwork(request, callback);
+        verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(request.networkCapabilities),
+                eq(REQUEST.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
+                eq(testPkgName), eq(testAttributionTag));
+        reset(mService);
+
+        // Verify that register network callback does not calls requestNetwork at all.
+        manager.registerNetworkCallback(request, callback);
+        verify(mService, never()).requestNetwork(anyInt(), any(), anyInt(), any(), anyInt(), any(),
+                anyInt(), anyInt(), any(), any());
+        verify(mService).listenForNetwork(eq(request.networkCapabilities), any(), any(), anyInt(),
+                eq(testPkgName), eq(testAttributionTag));
+        reset(mService);
+
+        Handler handler = new Handler(ConnectivityThread.getInstanceLooper());
+
+        manager.registerDefaultNetworkCallback(callback);
+        verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(null),
+                eq(TRACK_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
+                eq(testPkgName), eq(testAttributionTag));
+        reset(mService);
+
+        manager.registerDefaultNetworkCallbackForUid(42, callback, handler);
+        verify(mService).requestNetwork(eq(42), eq(null),
+                eq(TRACK_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
+                eq(testPkgName), eq(testAttributionTag));
+
+        manager.requestBackgroundNetwork(request, callback, handler);
+        verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(request.networkCapabilities),
+                eq(BACKGROUND_REQUEST.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
+                eq(testPkgName), eq(testAttributionTag));
+        reset(mService);
+
+        manager.registerSystemDefaultNetworkCallback(callback, handler);
+        verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(null),
+                eq(TRACK_SYSTEM_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
+                eq(testPkgName), eq(testAttributionTag));
+        reset(mService);
+    }
+
+    static Message makeMessage(NetworkRequest req, int messageType) {
+        Bundle bundle = new Bundle();
+        bundle.putParcelable(NetworkRequest.class.getSimpleName(), req);
+        // Pass default objects as we don't care which get passed here
+        bundle.putParcelable(Network.class.getSimpleName(), new Network(1));
+        bundle.putParcelable(NetworkCapabilities.class.getSimpleName(), new NetworkCapabilities());
+        bundle.putParcelable(LinkProperties.class.getSimpleName(), new LinkProperties());
+        Message msg = Message.obtain();
+        msg.what = messageType;
+        msg.setData(bundle);
+        return msg;
+    }
+
+    static NetworkRequest makeRequest(int requestId) {
+        NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
+        return new NetworkRequest(request.networkCapabilities, ConnectivityManager.TYPE_NONE,
+                requestId, NetworkRequest.Type.NONE);
+    }
+
+    static void expectThrowable(Runnable block, Class<? extends Throwable> throwableType) {
+        try {
+            block.run();
+        } catch (Throwable t) {
+            if (t.getClass().equals(throwableType)) {
+                return;
+            }
+            fail("expected exception of type " + throwableType + ", but was " + t.getClass());
+        }
+        fail("expected exception of type " + throwableType);
+    }
+}
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
new file mode 100644
index 0000000..0707ef3
--- /dev/null
+++ b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
@@ -0,0 +1,447 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+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 android.test.mock.MockContext;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.net.VpnProfile;
+import com.android.internal.org.bouncycastle.x509.X509V1CertificateGenerator;
+import com.android.net.module.util.ProxyUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+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;
+import java.util.concurrent.TimeUnit;
+
+import javax.security.auth.x500.X500Principal;
+
+/** Unit tests for {@link Ikev2VpnProfile.Builder}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class Ikev2VpnProfileTest {
+    private static final String SERVER_ADDR_STRING = "1.2.3.4";
+    private static final String IDENTITY_STRING = "Identity";
+    private static final String USERNAME_STRING = "username";
+    private static final String PASSWORD_STRING = "pa55w0rd";
+    private static final String EXCL_LIST = "exclList";
+    private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
+    private static final int TEST_MTU = 1300;
+
+    private final MockContext mMockContext =
+            new MockContext() {
+                @Override
+                public String getOpPackageName() {
+                    return "fooPackage";
+                }
+            };
+    private final ProxyInfo mProxy = ProxyInfo.buildDirectProxy(
+            SERVER_ADDR_STRING, -1, ProxyUtils.exclusionStringAsList(EXCL_LIST));
+
+    private X509Certificate mUserCert;
+    private X509Certificate mServerRootCa;
+    private PrivateKey mPrivateKey;
+
+    @Before
+    public void setUp() throws Exception {
+        mServerRootCa = generateRandomCertAndKeyPair().cert;
+
+        final CertificateAndKey userCertKey = generateRandomCertAndKeyPair();
+        mUserCert = userCertKey.cert;
+        mPrivateKey = userCertKey.key;
+    }
+
+    private Ikev2VpnProfile.Builder getBuilderWithDefaultOptions() {
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING);
+
+        builder.setBypassable(true);
+        builder.setProxy(mProxy);
+        builder.setMaxMtu(TEST_MTU);
+        builder.setMetered(true);
+
+        return builder;
+    }
+
+    @Test
+    public void testBuildValidProfileWithOptions() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+
+        // Check non-auth parameters correctly stored
+        assertEquals(SERVER_ADDR_STRING, profile.getServerAddr());
+        assertEquals(IDENTITY_STRING, profile.getUserIdentity());
+        assertEquals(mProxy, profile.getProxyInfo());
+        assertTrue(profile.isBypassable());
+        assertTrue(profile.isMetered());
+        assertEquals(TEST_MTU, profile.getMaxMtu());
+        assertEquals(Ikev2VpnProfile.DEFAULT_ALGORITHMS, profile.getAllowedAlgorithms());
+    }
+
+    @Test
+    public void testBuildUsernamePasswordProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+
+        assertEquals(USERNAME_STRING, profile.getUsername());
+        assertEquals(PASSWORD_STRING, profile.getPassword());
+        assertEquals(mServerRootCa, profile.getServerRootCaCert());
+
+        assertNull(profile.getPresharedKey());
+        assertNull(profile.getRsaPrivateKey());
+        assertNull(profile.getUserCert());
+    }
+
+    @Test
+    public void testBuildDigitalSignatureProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+
+        assertEquals(profile.getUserCert(), mUserCert);
+        assertEquals(mPrivateKey, profile.getRsaPrivateKey());
+        assertEquals(profile.getServerRootCaCert(), mServerRootCa);
+
+        assertNull(profile.getPresharedKey());
+        assertNull(profile.getUsername());
+        assertNull(profile.getPassword());
+    }
+
+    @Test
+    public void testBuildPresharedKeyProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthPsk(PSK_BYTES);
+        final Ikev2VpnProfile profile = builder.build();
+        assertNotNull(profile);
+
+        assertArrayEquals(PSK_BYTES, profile.getPresharedKey());
+
+        assertNull(profile.getServerRootCaCert());
+        assertNull(profile.getUsername());
+        assertNull(profile.getPassword());
+        assertNull(profile.getRsaPrivateKey());
+        assertNull(profile.getUserCert());
+    }
+
+    @Test
+    public void testBuildWithAllowedAlgorithmsAead() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAuthPsk(PSK_BYTES);
+
+        List<String> allowedAlgorithms =
+                Arrays.asList(
+                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
+                        IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305);
+        builder.setAllowedAlgorithms(allowedAlgorithms);
+
+        final Ikev2VpnProfile profile = builder.build();
+        assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms());
+    }
+
+    @Test
+    public void testBuildWithAllowedAlgorithmsNormal() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        builder.setAuthPsk(PSK_BYTES);
+
+        List<String> allowedAlgorithms =
+                Arrays.asList(
+                        IpSecAlgorithm.AUTH_HMAC_SHA512,
+                        IpSecAlgorithm.AUTH_AES_XCBC,
+                        IpSecAlgorithm.AUTH_AES_CMAC,
+                        IpSecAlgorithm.CRYPT_AES_CBC,
+                        IpSecAlgorithm.CRYPT_AES_CTR);
+        builder.setAllowedAlgorithms(allowedAlgorithms);
+
+        final Ikev2VpnProfile profile = builder.build();
+        assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms());
+    }
+
+    @Test
+    public void testSetAllowedAlgorithmsEmptyList() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.setAllowedAlgorithms(new ArrayList<>());
+            fail("Expected exception due to no valid algorithm set");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testSetAllowedAlgorithmsInvalidList() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        List<String> allowedAlgorithms = new ArrayList<>();
+
+        try {
+            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA256));
+            fail("Expected exception due to missing encryption");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        try {
+            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.CRYPT_AES_CBC));
+            fail("Expected exception due to missing authentication");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testSetAllowedAlgorithmsInsecureAlgorithm() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+        List<String> allowedAlgorithms = new ArrayList<>();
+
+        try {
+            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_MD5));
+            fail("Expected exception due to insecure algorithm");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        try {
+            builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA1));
+            fail("Expected exception due to insecure algorithm");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testBuildNoAuthMethodSet() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.build();
+            fail("Expected exception due to lack of auth method");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testBuildInvalidMtu() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        try {
+            builder.setMaxMtu(500);
+            fail("Expected exception due to too-small MTU");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    private void verifyVpnProfileCommon(VpnProfile profile) {
+        assertEquals(SERVER_ADDR_STRING, profile.server);
+        assertEquals(IDENTITY_STRING, profile.ipsecIdentifier);
+        assertEquals(mProxy, profile.proxy);
+        assertTrue(profile.isBypassable);
+        assertTrue(profile.isMetered);
+        assertEquals(TEST_MTU, profile.maxMtu);
+    }
+
+    @Test
+    public void testPskConvertToVpnProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthPsk(PSK_BYTES);
+        final VpnProfile profile = builder.build().toVpnProfile();
+
+        verifyVpnProfileCommon(profile);
+        assertEquals(Ikev2VpnProfile.encodeForIpsecSecret(PSK_BYTES), profile.ipsecSecret);
+
+        // Check nothing else is set
+        assertEquals("", profile.username);
+        assertEquals("", profile.password);
+        assertEquals("", profile.ipsecUserCert);
+        assertEquals("", profile.ipsecCaCert);
+    }
+
+    @Test
+    public void testUsernamePasswordConvertToVpnProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final VpnProfile profile = builder.build().toVpnProfile();
+
+        verifyVpnProfileCommon(profile);
+        assertEquals(USERNAME_STRING, profile.username);
+        assertEquals(PASSWORD_STRING, profile.password);
+        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);
+
+        // Check nothing else is set
+        assertEquals("", profile.ipsecUserCert);
+        assertEquals("", profile.ipsecSecret);
+    }
+
+    @Test
+    public void testRsaConvertToVpnProfile() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final VpnProfile profile = builder.build().toVpnProfile();
+
+        final String expectedSecret = Ikev2VpnProfile.PREFIX_INLINE
+                + Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded());
+        verifyVpnProfileCommon(profile);
+        assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert);
+        assertEquals(
+                expectedSecret,
+                profile.ipsecSecret);
+        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);
+
+        // Check nothing else is set
+        assertEquals("", profile.username);
+        assertEquals("", profile.password);
+    }
+
+    @Test
+    public void testPskFromVpnProfileDiscardsIrrelevantValues() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthPsk(PSK_BYTES);
+        final VpnProfile profile = builder.build().toVpnProfile();
+        profile.username = USERNAME_STRING;
+        profile.password = PASSWORD_STRING;
+        profile.ipsecCaCert = Ikev2VpnProfile.certificateToPemString(mServerRootCa);
+        profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);
+
+        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
+        assertNull(result.getUsername());
+        assertNull(result.getPassword());
+        assertNull(result.getUserCert());
+        assertNull(result.getRsaPrivateKey());
+        assertNull(result.getServerRootCaCert());
+    }
+
+    @Test
+    public void testUsernamePasswordFromVpnProfileDiscardsIrrelevantValues() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final VpnProfile profile = builder.build().toVpnProfile();
+        profile.ipsecSecret = new String(PSK_BYTES);
+        profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);
+
+        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
+        assertNull(result.getPresharedKey());
+        assertNull(result.getUserCert());
+        assertNull(result.getRsaPrivateKey());
+    }
+
+    @Test
+    public void testRsaFromVpnProfileDiscardsIrrelevantValues() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final VpnProfile profile = builder.build().toVpnProfile();
+        profile.username = USERNAME_STRING;
+        profile.password = PASSWORD_STRING;
+
+        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
+        assertNull(result.getUsername());
+        assertNull(result.getPassword());
+        assertNull(result.getPresharedKey());
+    }
+
+    @Test
+    public void testPskConversionIsLossless() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthPsk(PSK_BYTES);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testUsernamePasswordConversionIsLossless() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    @Test
+    public void testRsaConversionIsLossless() throws Exception {
+        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();
+
+        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
+        final Ikev2VpnProfile ikeProfile = builder.build();
+
+        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
+    }
+
+    private static class CertificateAndKey {
+        public final X509Certificate cert;
+        public final PrivateKey key;
+
+        CertificateAndKey(X509Certificate cert, PrivateKey key) {
+            this.cert = cert;
+            this.key = key;
+        }
+    }
+
+    private static CertificateAndKey generateRandomCertAndKeyPair() throws Exception {
+        final Date validityBeginDate =
+                new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1L));
+        final Date validityEndDate =
+                new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1L));
+
+        // Generate a keypair
+        final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+        keyPairGenerator.initialize(512);
+        final KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+        final X500Principal dnName = new X500Principal("CN=test.android.com");
+        final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
+        certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
+        certGen.setSubjectDN(dnName);
+        certGen.setIssuerDN(dnName);
+        certGen.setNotBefore(validityBeginDate);
+        certGen.setNotAfter(validityEndDate);
+        certGen.setPublicKey(keyPair.getPublic());
+        certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
+
+        final X509Certificate cert = certGen.generate(keyPair.getPrivate(), "AndroidOpenSSL");
+        return new CertificateAndKey(cert, keyPair.getPrivate());
+    }
+}
diff --git a/tests/unit/java/android/net/IpMemoryStoreTest.java b/tests/unit/java/android/net/IpMemoryStoreTest.java
new file mode 100644
index 0000000..0b13800
--- /dev/null
+++ b/tests/unit/java/android/net/IpMemoryStoreTest.java
@@ -0,0 +1,332 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.ipmemorystore.Blob;
+import android.net.ipmemorystore.IOnStatusListener;
+import android.net.ipmemorystore.NetworkAttributes;
+import android.net.ipmemorystore.NetworkAttributesParcelable;
+import android.net.ipmemorystore.Status;
+import android.net.networkstack.ModuleNetworkStackClient;
+import android.os.RemoteException;
+
+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.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.UnknownHostException;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpMemoryStoreTest {
+    private static final String TAG = IpMemoryStoreTest.class.getSimpleName();
+    private static final String TEST_CLIENT_ID = "testClientId";
+    private static final String TEST_DATA_NAME = "testData";
+    private static final String TEST_OTHER_DATA_NAME = TEST_DATA_NAME + "Other";
+    private static final byte[] TEST_BLOB_DATA = new byte[] { -3, 6, 8, -9, 12,
+            -128, 0, 89, 112, 91, -34 };
+    private static final NetworkAttributes TEST_NETWORK_ATTRIBUTES = buildTestNetworkAttributes(
+            "hint", 219);
+
+    @Mock
+    Context mMockContext;
+    @Mock
+    ModuleNetworkStackClient mModuleNetworkStackClient;
+    @Mock
+    IIpMemoryStore mMockService;
+    @Mock
+    IOnStatusListener mIOnStatusListener;
+    IpMemoryStore mStore;
+
+    @Captor
+    ArgumentCaptor<IIpMemoryStoreCallbacks> mCbCaptor;
+    @Captor
+    ArgumentCaptor<NetworkAttributesParcelable> mNapCaptor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    private void startIpMemoryStore(boolean supplyService) {
+        if (supplyService) {
+            doAnswer(invocation -> {
+                ((IIpMemoryStoreCallbacks) invocation.getArgument(0))
+                        .onIpMemoryStoreFetched(mMockService);
+                return null;
+            }).when(mModuleNetworkStackClient).fetchIpMemoryStore(any());
+        } else {
+            doNothing().when(mModuleNetworkStackClient).fetchIpMemoryStore(mCbCaptor.capture());
+        }
+        mStore = new IpMemoryStore(mMockContext) {
+            @Override
+            protected ModuleNetworkStackClient getModuleNetworkStackClient(Context ctx) {
+                return mModuleNetworkStackClient;
+            }
+        };
+    }
+
+    private static NetworkAttributes buildTestNetworkAttributes(String hint, int mtu) {
+        return new NetworkAttributes.Builder()
+                .setCluster(hint)
+                .setMtu(mtu)
+                .build();
+    }
+
+    @Test
+    public void testNetworkAttributes() throws Exception {
+        startIpMemoryStore(true /* supplyService */);
+        final String l2Key = "fakeKey";
+
+        mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES,
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
+        verify(mMockService, times(1)).storeNetworkAttributes(eq(l2Key),
+                mNapCaptor.capture(), any());
+        assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue()));
+
+        mStore.retrieveNetworkAttributes(l2Key,
+                (status, key, attr) -> {
+                    assertTrue("Retrieve network attributes not successful : "
+                            + status.resultCode, status.isSuccess());
+                    assertEquals(l2Key, key);
+                    assertEquals(TEST_NETWORK_ATTRIBUTES, attr);
+                });
+
+        verify(mMockService, times(1)).retrieveNetworkAttributes(eq(l2Key), any());
+    }
+
+    @Test
+    public void testPrivateData() throws RemoteException {
+        startIpMemoryStore(true /* supplyService */);
+        final Blob b = new Blob();
+        b.data = TEST_BLOB_DATA;
+        final String l2Key = "fakeKey";
+
+        mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b,
+                status -> {
+                    assertTrue("Store not successful : " + status.resultCode, status.isSuccess());
+                });
+        verify(mMockService, times(1)).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME),
+                eq(b), any());
+
+        mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME,
+                (status, key, name, data) -> {
+                    assertTrue("Retrieve blob status not successful : " + status.resultCode,
+                            status.isSuccess());
+                    assertEquals(l2Key, key);
+                    assertEquals(name, TEST_DATA_NAME);
+                    assertTrue(Arrays.equals(b.data, data.data));
+                });
+        verify(mMockService, times(1)).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID),
+                eq(TEST_OTHER_DATA_NAME), any());
+    }
+
+    @Test
+    public void testFindL2Key()
+            throws UnknownHostException, RemoteException, Exception {
+        startIpMemoryStore(true /* supplyService */);
+        final String l2Key = "fakeKey";
+
+        mStore.findL2Key(TEST_NETWORK_ATTRIBUTES,
+                (status, key) -> {
+                    assertTrue("Retrieve network sameness not successful : " + status.resultCode,
+                            status.isSuccess());
+                    assertEquals(l2Key, key);
+                });
+        verify(mMockService, times(1)).findL2Key(mNapCaptor.capture(), any());
+        assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue()));
+    }
+
+    @Test
+    public void testIsSameNetwork() throws UnknownHostException, RemoteException {
+        startIpMemoryStore(true /* supplyService */);
+        final String l2Key1 = "fakeKey1";
+        final String l2Key2 = "fakeKey2";
+
+        mStore.isSameNetwork(l2Key1, l2Key2,
+                (status, answer) -> {
+                    assertFalse("Retrieve network sameness suspiciously successful : "
+                            + status.resultCode, status.isSuccess());
+                    assertEquals(Status.ERROR_ILLEGAL_ARGUMENT, status.resultCode);
+                    assertNull(answer);
+                });
+        verify(mMockService, times(1)).isSameNetwork(eq(l2Key1), eq(l2Key2), any());
+    }
+
+    @Test
+    public void testEnqueuedIpMsRequests() throws Exception {
+        startIpMemoryStore(false /* supplyService */);
+
+        final Blob b = new Blob();
+        b.data = TEST_BLOB_DATA;
+        final String l2Key = "fakeKey";
+
+        // enqueue multiple ipms requests
+        mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES,
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
+        mStore.retrieveNetworkAttributes(l2Key,
+                (status, key, attr) -> {
+                    assertTrue("Retrieve network attributes not successful : "
+                            + status.resultCode, status.isSuccess());
+                    assertEquals(l2Key, key);
+                    assertEquals(TEST_NETWORK_ATTRIBUTES, attr);
+                });
+        mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b,
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
+        mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME,
+                (status, key, name, data) -> {
+                    assertTrue("Retrieve blob status not successful : " + status.resultCode,
+                            status.isSuccess());
+                    assertEquals(l2Key, key);
+                    assertEquals(name, TEST_DATA_NAME);
+                    assertTrue(Arrays.equals(b.data, data.data));
+                });
+
+        // get ipms service ready
+        mCbCaptor.getValue().onIpMemoryStoreFetched(mMockService);
+
+        InOrder inOrder = inOrder(mMockService);
+
+        inOrder.verify(mMockService).storeNetworkAttributes(eq(l2Key), mNapCaptor.capture(), any());
+        inOrder.verify(mMockService).retrieveNetworkAttributes(eq(l2Key), any());
+        inOrder.verify(mMockService).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME),
+                eq(b), any());
+        inOrder.verify(mMockService).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID),
+                eq(TEST_OTHER_DATA_NAME), any());
+        assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue()));
+    }
+
+    @Test
+    public void testEnqueuedIpMsRequestsWithException() throws Exception {
+        startIpMemoryStore(true /* supplyService */);
+        doThrow(RemoteException.class).when(mMockService).retrieveNetworkAttributes(any(), any());
+
+        final Blob b = new Blob();
+        b.data = TEST_BLOB_DATA;
+        final String l2Key = "fakeKey";
+
+        // enqueue multiple ipms requests
+        mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES,
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
+        mStore.retrieveNetworkAttributes(l2Key,
+                (status, key, attr) -> {
+                    assertTrue("Retrieve network attributes not successful : "
+                            + status.resultCode, status.isSuccess());
+                    assertEquals(l2Key, key);
+                    assertEquals(TEST_NETWORK_ATTRIBUTES, attr);
+                });
+        mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b,
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
+        mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME,
+                (status, key, name, data) -> {
+                    assertTrue("Retrieve blob status not successful : " + status.resultCode,
+                            status.isSuccess());
+                    assertEquals(l2Key, key);
+                    assertEquals(name, TEST_DATA_NAME);
+                    assertTrue(Arrays.equals(b.data, data.data));
+                });
+
+        // verify the rest of the queue is still processed in order even if the remote exception
+        // occurs when calling one or more requests
+        InOrder inOrder = inOrder(mMockService);
+
+        inOrder.verify(mMockService).storeNetworkAttributes(eq(l2Key), mNapCaptor.capture(), any());
+        inOrder.verify(mMockService).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME),
+                eq(b), any());
+        inOrder.verify(mMockService).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID),
+                eq(TEST_OTHER_DATA_NAME), any());
+        assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue()));
+    }
+
+    @Test
+    public void testEnqueuedIpMsRequestsCallbackFunctionWithException() throws Exception {
+        startIpMemoryStore(true /* supplyService */);
+
+        final Blob b = new Blob();
+        b.data = TEST_BLOB_DATA;
+        final String l2Key = "fakeKey";
+
+        // enqueue multiple ipms requests
+        mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES,
+                status -> assertTrue("Store not successful : " + status.resultCode,
+                        status.isSuccess()));
+        mStore.retrieveNetworkAttributes(l2Key,
+                (status, key, attr) -> {
+                    throw new RuntimeException("retrieveNetworkAttributes test");
+                });
+        mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b,
+                status -> {
+                    throw new RuntimeException("storeBlob test");
+                });
+        mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME,
+                (status, key, name, data) -> {
+                    assertTrue("Retrieve blob status not successful : " + status.resultCode,
+                            status.isSuccess());
+                    assertEquals(l2Key, key);
+                    assertEquals(name, TEST_DATA_NAME);
+                    assertTrue(Arrays.equals(b.data, data.data));
+                });
+
+        // verify the rest of the queue is still processed in order even if when one or more
+        // callback throw the remote exception
+        InOrder inOrder = inOrder(mMockService);
+
+        inOrder.verify(mMockService).storeNetworkAttributes(eq(l2Key), mNapCaptor.capture(),
+                any());
+        inOrder.verify(mMockService).retrieveNetworkAttributes(eq(l2Key), any());
+        inOrder.verify(mMockService).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME),
+                eq(b), any());
+        inOrder.verify(mMockService).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID),
+                eq(TEST_OTHER_DATA_NAME), any());
+        assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue()));
+    }
+
+    @Test
+    public void testFactoryReset() throws RemoteException {
+        startIpMemoryStore(true /* supplyService */);
+        mStore.factoryReset();
+        verify(mMockService, times(1)).factoryReset();
+    }
+}
diff --git a/tests/unit/java/android/net/IpSecAlgorithmTest.java b/tests/unit/java/android/net/IpSecAlgorithmTest.java
new file mode 100644
index 0000000..3a8d600
--- /dev/null
+++ b/tests/unit/java/android/net/IpSecAlgorithmTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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.IpSecAlgorithm.ALGO_TO_REQUIRED_FIRST_SDK;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.CollectionUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.AbstractMap.SimpleEntry;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Random;
+import java.util.Set;
+
+/** Unit tests for {@link IpSecAlgorithm}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class IpSecAlgorithmTest {
+    private static final byte[] KEY_MATERIAL;
+
+    private final Resources mMockResources = mock(Resources.class);
+
+    static {
+        KEY_MATERIAL = new byte[128];
+        new Random().nextBytes(KEY_MATERIAL);
+    };
+
+    private static byte[] generateKey(int keyLenInBits) {
+        return Arrays.copyOf(KEY_MATERIAL, keyLenInBits / 8);
+    }
+
+    @Test
+    public void testNoTruncLen() throws Exception {
+        Entry<String, Integer>[] authAndAeadList =
+                new Entry[] {
+                    new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_MD5, 128),
+                    new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_SHA1, 160),
+                    new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_SHA256, 256),
+                    new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_SHA384, 384),
+                    new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_SHA512, 512),
+                    new SimpleEntry<>(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, 224),
+                };
+
+        // Expect auth and aead algorithms to throw errors if trunclen is omitted.
+        for (Entry<String, Integer> algData : authAndAeadList) {
+            try {
+                new IpSecAlgorithm(
+                        algData.getKey(), Arrays.copyOf(KEY_MATERIAL, algData.getValue() / 8));
+                fail("Expected exception on unprovided auth trunclen");
+            } catch (IllegalArgumentException expected) {
+            }
+        }
+
+        // Ensure crypt works with no truncation length supplied.
+        new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, Arrays.copyOf(KEY_MATERIAL, 256 / 8));
+    }
+
+    private void checkAuthKeyAndTruncLenValidation(String algoName, int keyLen, int truncLen)
+            throws Exception {
+        new IpSecAlgorithm(algoName, generateKey(keyLen), truncLen);
+
+        try {
+            new IpSecAlgorithm(algoName, generateKey(keyLen));
+            fail("Expected exception on unprovided auth trunclen");
+        } catch (IllegalArgumentException pass) {
+        }
+
+        try {
+            new IpSecAlgorithm(algoName, generateKey(keyLen + 8), truncLen);
+            fail("Invalid key length not validated");
+        } catch (IllegalArgumentException pass) {
+        }
+
+        try {
+            new IpSecAlgorithm(algoName, generateKey(keyLen), truncLen + 1);
+            fail("Invalid truncation length not validated");
+        } catch (IllegalArgumentException pass) {
+        }
+    }
+
+    private void checkCryptKeyLenValidation(String algoName, int keyLen) throws Exception {
+        new IpSecAlgorithm(algoName, generateKey(keyLen));
+
+        try {
+            new IpSecAlgorithm(algoName, generateKey(keyLen + 8));
+            fail("Invalid key length not validated");
+        } catch (IllegalArgumentException pass) {
+        }
+    }
+
+    @Test
+    public void testValidationForAlgosAddedInS() throws Exception {
+        if (Build.VERSION.FIRST_SDK_INT <= Build.VERSION_CODES.R) {
+            return;
+        }
+
+        for (int len : new int[] {160, 224, 288}) {
+            checkCryptKeyLenValidation(IpSecAlgorithm.CRYPT_AES_CTR, len);
+        }
+        checkAuthKeyAndTruncLenValidation(IpSecAlgorithm.AUTH_AES_XCBC, 128, 96);
+        checkAuthKeyAndTruncLenValidation(IpSecAlgorithm.AUTH_AES_CMAC, 128, 96);
+        checkAuthKeyAndTruncLenValidation(IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305, 288, 128);
+    }
+
+    @Test
+    public void testTruncLenValidation() throws Exception {
+        for (int truncLen : new int[] {256, 512}) {
+            new IpSecAlgorithm(
+                    IpSecAlgorithm.AUTH_HMAC_SHA512,
+                    Arrays.copyOf(KEY_MATERIAL, 512 / 8),
+                    truncLen);
+        }
+
+        for (int truncLen : new int[] {255, 513}) {
+            try {
+                new IpSecAlgorithm(
+                        IpSecAlgorithm.AUTH_HMAC_SHA512,
+                        Arrays.copyOf(KEY_MATERIAL, 512 / 8),
+                        truncLen);
+                fail("Invalid truncation length not validated");
+            } catch (IllegalArgumentException pass) {
+            }
+        }
+    }
+
+    @Test
+    public void testLenValidation() throws Exception {
+        for (int len : new int[] {128, 192, 256}) {
+            new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, Arrays.copyOf(KEY_MATERIAL, len / 8));
+        }
+        try {
+            new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, Arrays.copyOf(KEY_MATERIAL, 384 / 8));
+            fail("Invalid key length not validated");
+        } catch (IllegalArgumentException pass) {
+        }
+    }
+
+    @Test
+    public void testAlgoNameValidation() throws Exception {
+        try {
+            new IpSecAlgorithm("rot13", Arrays.copyOf(KEY_MATERIAL, 128 / 8));
+            fail("Invalid algorithm name not validated");
+        } catch (IllegalArgumentException pass) {
+        }
+    }
+
+    @Test
+    public void testParcelUnparcel() throws Exception {
+        IpSecAlgorithm init =
+                new IpSecAlgorithm(
+                        IpSecAlgorithm.AUTH_HMAC_SHA512, Arrays.copyOf(KEY_MATERIAL, 512 / 8), 256);
+
+        Parcel p = Parcel.obtain();
+        p.setDataPosition(0);
+        init.writeToParcel(p, 0);
+
+        p.setDataPosition(0);
+        IpSecAlgorithm fin = IpSecAlgorithm.CREATOR.createFromParcel(p);
+        assertTrue("Parcel/Unparcel failed!", IpSecAlgorithm.equals(init, fin));
+        p.recycle();
+    }
+
+    private static Set<String> getMandatoryAlgos() {
+        return CollectionUtils.filter(
+                ALGO_TO_REQUIRED_FIRST_SDK.keySet(),
+                i -> Build.VERSION.FIRST_SDK_INT >= ALGO_TO_REQUIRED_FIRST_SDK.get(i));
+    }
+
+    private static Set<String> getOptionalAlgos() {
+        return CollectionUtils.filter(
+                ALGO_TO_REQUIRED_FIRST_SDK.keySet(),
+                i -> Build.VERSION.FIRST_SDK_INT < ALGO_TO_REQUIRED_FIRST_SDK.get(i));
+    }
+
+    @Test
+    public void testGetSupportedAlgorithms() throws Exception {
+        assertTrue(IpSecAlgorithm.getSupportedAlgorithms().containsAll(getMandatoryAlgos()));
+        assertTrue(ALGO_TO_REQUIRED_FIRST_SDK.keySet().containsAll(
+                IpSecAlgorithm.getSupportedAlgorithms()));
+    }
+
+    @Test
+    public void testLoadAlgos() throws Exception {
+        final Set<String> optionalAlgoSet = getOptionalAlgos();
+        final String[] optionalAlgos = optionalAlgoSet.toArray(new String[0]);
+
+        doReturn(optionalAlgos).when(mMockResources)
+                .getStringArray(com.android.internal.R.array.config_optionalIpSecAlgorithms);
+
+        final Set<String> enabledAlgos = new HashSet<>(IpSecAlgorithm.loadAlgos(mMockResources));
+        final Set<String> expectedAlgos = ALGO_TO_REQUIRED_FIRST_SDK.keySet();
+
+        assertEquals(expectedAlgos, enabledAlgos);
+    }
+}
diff --git a/tests/unit/java/android/net/IpSecConfigTest.java b/tests/unit/java/android/net/IpSecConfigTest.java
new file mode 100644
index 0000000..25e225e
--- /dev/null
+++ b/tests/unit/java/android/net/IpSecConfigTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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 com.android.testutils.ParcelUtils.assertParcelSane;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link IpSecConfig}. */
+@SmallTest
+@RunWith(JUnit4.class)
+public class IpSecConfigTest {
+
+    @Test
+    public void testDefaults() throws Exception {
+        IpSecConfig c = new IpSecConfig();
+        assertEquals(IpSecTransform.MODE_TRANSPORT, c.getMode());
+        assertEquals("", c.getSourceAddress());
+        assertEquals("", c.getDestinationAddress());
+        assertNull(c.getNetwork());
+        assertEquals(IpSecTransform.ENCAP_NONE, c.getEncapType());
+        assertEquals(IpSecManager.INVALID_RESOURCE_ID, c.getEncapSocketResourceId());
+        assertEquals(0, c.getEncapRemotePort());
+        assertEquals(0, c.getNattKeepaliveInterval());
+        assertNull(c.getEncryption());
+        assertNull(c.getAuthentication());
+        assertEquals(IpSecManager.INVALID_RESOURCE_ID, c.getSpiResourceId());
+        assertEquals(0, c.getXfrmInterfaceId());
+    }
+
+    private IpSecConfig getSampleConfig() {
+        IpSecConfig c = new IpSecConfig();
+        c.setMode(IpSecTransform.MODE_TUNNEL);
+        c.setSourceAddress("0.0.0.0");
+        c.setDestinationAddress("1.2.3.4");
+        c.setSpiResourceId(1984);
+        c.setEncryption(
+                new IpSecAlgorithm(
+                        IpSecAlgorithm.CRYPT_AES_CBC,
+                        new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}));
+        c.setAuthentication(
+                new IpSecAlgorithm(
+                        IpSecAlgorithm.AUTH_HMAC_MD5,
+                        new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0},
+                        128));
+        c.setAuthenticatedEncryption(
+                new IpSecAlgorithm(
+                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
+                        new byte[] {
+                            1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0, 1, 2, 3, 4
+                        },
+                        128));
+        c.setEncapType(android.system.OsConstants.UDP_ENCAP_ESPINUDP);
+        c.setEncapSocketResourceId(7);
+        c.setEncapRemotePort(22);
+        c.setNattKeepaliveInterval(42);
+        c.setMarkValue(12);
+        c.setMarkMask(23);
+        c.setXfrmInterfaceId(34);
+
+        return c;
+    }
+
+    @Test
+    public void testCopyConstructor() {
+        IpSecConfig original = getSampleConfig();
+        IpSecConfig copy = new IpSecConfig(original);
+
+        assertEquals(original, copy);
+        assertNotSame(original, copy);
+    }
+
+    @Test
+    public void testParcelUnparcel() {
+        assertParcelingIsLossless(new IpSecConfig());
+
+        IpSecConfig c = getSampleConfig();
+        assertParcelSane(c, 15);
+    }
+}
diff --git a/tests/unit/java/android/net/IpSecManagerTest.java b/tests/unit/java/android/net/IpSecManagerTest.java
new file mode 100644
index 0000000..730e2d5
--- /dev/null
+++ b/tests/unit/java/android/net/IpSecManagerTest.java
@@ -0,0 +1,305 @@
+/*
+ * 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.system.OsConstants.AF_INET;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.system.Os;
+import android.test.mock.MockContext;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.IpSecService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+/** Unit tests for {@link IpSecManager}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class IpSecManagerTest {
+
+    private static final int TEST_UDP_ENCAP_PORT = 34567;
+    private static final int DROID_SPI = 0xD1201D;
+    private static final int DUMMY_RESOURCE_ID = 0x1234;
+
+    private static final InetAddress GOOGLE_DNS_4;
+    private static final String VTI_INTF_NAME = "ipsec_test";
+    private static final InetAddress VTI_LOCAL_ADDRESS;
+    private static final LinkAddress VTI_INNER_ADDRESS = new LinkAddress("10.0.1.1/24");
+
+    static {
+        try {
+            // Google Public DNS Addresses;
+            GOOGLE_DNS_4 = InetAddress.getByName("8.8.8.8");
+            VTI_LOCAL_ADDRESS = InetAddress.getByName("8.8.4.4");
+        } catch (UnknownHostException e) {
+            throw new RuntimeException("Could not resolve DNS Addresses", e);
+        }
+    }
+
+    private IpSecService mMockIpSecService;
+    private IpSecManager mIpSecManager;
+    private MockContext mMockContext = new MockContext() {
+        @Override
+        public String getOpPackageName() {
+            return "fooPackage";
+        }
+    };
+
+    @Before
+    public void setUp() throws Exception {
+        mMockIpSecService = mock(IpSecService.class);
+        mIpSecManager = new IpSecManager(mMockContext, mMockIpSecService);
+    }
+
+    /*
+     * Allocate a specific SPI
+     * Close SPIs
+     */
+    @Test
+    public void testAllocSpi() throws Exception {
+        IpSecSpiResponse spiResp =
+                new IpSecSpiResponse(IpSecManager.Status.OK, DUMMY_RESOURCE_ID, DROID_SPI);
+        when(mMockIpSecService.allocateSecurityParameterIndex(
+                        eq(GOOGLE_DNS_4.getHostAddress()),
+                        eq(DROID_SPI),
+                        anyObject()))
+                .thenReturn(spiResp);
+
+        IpSecManager.SecurityParameterIndex droidSpi =
+                mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4, DROID_SPI);
+        assertEquals(DROID_SPI, droidSpi.getSpi());
+
+        droidSpi.close();
+
+        verify(mMockIpSecService).releaseSecurityParameterIndex(DUMMY_RESOURCE_ID);
+    }
+
+    @Test
+    public void testAllocRandomSpi() throws Exception {
+        IpSecSpiResponse spiResp =
+                new IpSecSpiResponse(IpSecManager.Status.OK, DUMMY_RESOURCE_ID, DROID_SPI);
+        when(mMockIpSecService.allocateSecurityParameterIndex(
+                        eq(GOOGLE_DNS_4.getHostAddress()),
+                        eq(IpSecManager.INVALID_SECURITY_PARAMETER_INDEX),
+                        anyObject()))
+                .thenReturn(spiResp);
+
+        IpSecManager.SecurityParameterIndex randomSpi =
+                mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4);
+
+        assertEquals(DROID_SPI, randomSpi.getSpi());
+
+        randomSpi.close();
+
+        verify(mMockIpSecService).releaseSecurityParameterIndex(DUMMY_RESOURCE_ID);
+    }
+
+    /*
+     * Throws resource unavailable exception
+     */
+    @Test
+    public void testAllocSpiResUnavailableException() throws Exception {
+        IpSecSpiResponse spiResp =
+                new IpSecSpiResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE, 0, 0);
+        when(mMockIpSecService.allocateSecurityParameterIndex(
+                        anyString(), anyInt(), anyObject()))
+                .thenReturn(spiResp);
+
+        try {
+            mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4);
+            fail("ResourceUnavailableException was not thrown");
+        } catch (IpSecManager.ResourceUnavailableException e) {
+        }
+    }
+
+    /*
+     * Throws spi unavailable exception
+     */
+    @Test
+    public void testAllocSpiSpiUnavailableException() throws Exception {
+        IpSecSpiResponse spiResp = new IpSecSpiResponse(IpSecManager.Status.SPI_UNAVAILABLE, 0, 0);
+        when(mMockIpSecService.allocateSecurityParameterIndex(
+                        anyString(), anyInt(), anyObject()))
+                .thenReturn(spiResp);
+
+        try {
+            mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4);
+            fail("ResourceUnavailableException was not thrown");
+        } catch (IpSecManager.ResourceUnavailableException e) {
+        }
+    }
+
+    /*
+     * Should throw exception when request spi 0 in IpSecManager
+     */
+    @Test
+    public void testRequestAllocInvalidSpi() throws Exception {
+        try {
+            mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4, 0);
+            fail("Able to allocate invalid spi");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Test
+    public void testOpenEncapsulationSocket() throws Exception {
+        IpSecUdpEncapResponse udpEncapResp =
+                new IpSecUdpEncapResponse(
+                        IpSecManager.Status.OK,
+                        DUMMY_RESOURCE_ID,
+                        TEST_UDP_ENCAP_PORT,
+                        Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP));
+        when(mMockIpSecService.openUdpEncapsulationSocket(eq(TEST_UDP_ENCAP_PORT), anyObject()))
+                .thenReturn(udpEncapResp);
+
+        IpSecManager.UdpEncapsulationSocket encapSocket =
+                mIpSecManager.openUdpEncapsulationSocket(TEST_UDP_ENCAP_PORT);
+        assertNotNull(encapSocket.getFileDescriptor());
+        assertEquals(TEST_UDP_ENCAP_PORT, encapSocket.getPort());
+
+        encapSocket.close();
+
+        verify(mMockIpSecService).closeUdpEncapsulationSocket(DUMMY_RESOURCE_ID);
+    }
+
+    @Test
+    public void testApplyTransportModeTransformEnsuresSocketCreation() throws Exception {
+        Socket socket = new Socket();
+        IpSecConfig dummyConfig = new IpSecConfig();
+        IpSecTransform dummyTransform = new IpSecTransform(null, dummyConfig);
+
+        // Even if underlying SocketImpl is not initalized, this should force the init, and
+        // thereby succeed.
+        mIpSecManager.applyTransportModeTransform(
+                socket, IpSecManager.DIRECTION_IN, dummyTransform);
+
+        // Check to make sure the FileDescriptor is non-null
+        assertNotNull(socket.getFileDescriptor$());
+    }
+
+    @Test
+    public void testRemoveTransportModeTransformsForcesSocketCreation() throws Exception {
+        Socket socket = new Socket();
+
+        // Even if underlying SocketImpl is not initalized, this should force the init, and
+        // thereby succeed.
+        mIpSecManager.removeTransportModeTransforms(socket);
+
+        // Check to make sure the FileDescriptor is non-null
+        assertNotNull(socket.getFileDescriptor$());
+    }
+
+    @Test
+    public void testOpenEncapsulationSocketOnRandomPort() throws Exception {
+        IpSecUdpEncapResponse udpEncapResp =
+                new IpSecUdpEncapResponse(
+                        IpSecManager.Status.OK,
+                        DUMMY_RESOURCE_ID,
+                        TEST_UDP_ENCAP_PORT,
+                        Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP));
+
+        when(mMockIpSecService.openUdpEncapsulationSocket(eq(0), anyObject()))
+                .thenReturn(udpEncapResp);
+
+        IpSecManager.UdpEncapsulationSocket encapSocket =
+                mIpSecManager.openUdpEncapsulationSocket();
+
+        assertNotNull(encapSocket.getFileDescriptor());
+        assertEquals(TEST_UDP_ENCAP_PORT, encapSocket.getPort());
+
+        encapSocket.close();
+
+        verify(mMockIpSecService).closeUdpEncapsulationSocket(DUMMY_RESOURCE_ID);
+    }
+
+    @Test
+    public void testOpenEncapsulationSocketWithInvalidPort() throws Exception {
+        try {
+            mIpSecManager.openUdpEncapsulationSocket(IpSecManager.INVALID_SECURITY_PARAMETER_INDEX);
+            fail("IllegalArgumentException was not thrown");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    // TODO: add test when applicable transform builder interface is available
+
+    private IpSecManager.IpSecTunnelInterface createAndValidateVti(int resourceId, String intfName)
+            throws Exception {
+        IpSecTunnelInterfaceResponse dummyResponse =
+                new IpSecTunnelInterfaceResponse(IpSecManager.Status.OK, resourceId, intfName);
+        when(mMockIpSecService.createTunnelInterface(
+                eq(VTI_LOCAL_ADDRESS.getHostAddress()), eq(GOOGLE_DNS_4.getHostAddress()),
+                anyObject(), anyObject(), anyString()))
+                        .thenReturn(dummyResponse);
+
+        IpSecManager.IpSecTunnelInterface tunnelIntf = mIpSecManager.createIpSecTunnelInterface(
+                VTI_LOCAL_ADDRESS, GOOGLE_DNS_4, mock(Network.class));
+
+        assertNotNull(tunnelIntf);
+        return tunnelIntf;
+    }
+
+    @Test
+    public void testCreateVti() throws Exception {
+        IpSecManager.IpSecTunnelInterface tunnelIntf =
+                createAndValidateVti(DUMMY_RESOURCE_ID, VTI_INTF_NAME);
+
+        assertEquals(VTI_INTF_NAME, tunnelIntf.getInterfaceName());
+
+        tunnelIntf.close();
+        verify(mMockIpSecService).deleteTunnelInterface(eq(DUMMY_RESOURCE_ID), anyString());
+    }
+
+    @Test
+    public void testAddRemoveAddressesFromVti() throws Exception {
+        IpSecManager.IpSecTunnelInterface tunnelIntf =
+                createAndValidateVti(DUMMY_RESOURCE_ID, VTI_INTF_NAME);
+
+        tunnelIntf.addAddress(VTI_INNER_ADDRESS.getAddress(),
+                VTI_INNER_ADDRESS.getPrefixLength());
+        verify(mMockIpSecService)
+                .addAddressToTunnelInterface(
+                        eq(DUMMY_RESOURCE_ID), eq(VTI_INNER_ADDRESS), anyString());
+
+        tunnelIntf.removeAddress(VTI_INNER_ADDRESS.getAddress(),
+                VTI_INNER_ADDRESS.getPrefixLength());
+        verify(mMockIpSecService)
+                .addAddressToTunnelInterface(
+                        eq(DUMMY_RESOURCE_ID), eq(VTI_INNER_ADDRESS), anyString());
+    }
+}
diff --git a/tests/unit/java/android/net/IpSecTransformTest.java b/tests/unit/java/android/net/IpSecTransformTest.java
new file mode 100644
index 0000000..424f23d
--- /dev/null
+++ b/tests/unit/java/android/net/IpSecTransformTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link IpSecTransform}. */
+@SmallTest
+@RunWith(JUnit4.class)
+public class IpSecTransformTest {
+
+    @Test
+    public void testCreateTransformCopiesConfig() {
+        // Create a config with a few parameters to make sure it's not empty
+        IpSecConfig config = new IpSecConfig();
+        config.setSourceAddress("0.0.0.0");
+        config.setDestinationAddress("1.2.3.4");
+        config.setSpiResourceId(1984);
+
+        IpSecTransform preModification = new IpSecTransform(null, config);
+
+        config.setSpiResourceId(1985);
+        IpSecTransform postModification = new IpSecTransform(null, config);
+
+        assertNotEquals(preModification, postModification);
+    }
+
+    @Test
+    public void testCreateTransformsWithSameConfigEqual() {
+        // Create a config with a few parameters to make sure it's not empty
+        IpSecConfig config = new IpSecConfig();
+        config.setSourceAddress("0.0.0.0");
+        config.setDestinationAddress("1.2.3.4");
+        config.setSpiResourceId(1984);
+
+        IpSecTransform config1 = new IpSecTransform(null, config);
+        IpSecTransform config2 = new IpSecTransform(null, config);
+
+        assertEquals(config1, config2);
+    }
+}
diff --git a/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
new file mode 100644
index 0000000..bc6dbf2
--- /dev/null
+++ b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.util.KeepalivePacketDataUtil;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+@RunWith(JUnit4.class)
+public final class KeepalivePacketDataUtilTest {
+    private static final byte[] IPV4_KEEPALIVE_SRC_ADDR = {10, 0, 0, 1};
+    private static final byte[] IPV4_KEEPALIVE_DST_ADDR = {10, 0, 0, 5};
+
+    private Log.TerribleFailureHandler mOriginalHandler;
+
+    @Before
+    public void setUp() {
+        // Terrible failures are logged when using deprecated methods on newer platforms
+        mOriginalHandler = Log.setWtfHandler((tag, what, sys) ->
+                Log.e(tag, "Terrible failure in test", what));
+    }
+
+    @After
+    public void tearDown() {
+        Log.setWtfHandler(mOriginalHandler);
+    }
+
+    @Test
+    public void testFromTcpKeepaliveStableParcelable() throws Exception {
+        final int srcPort = 1234;
+        final int dstPort = 4321;
+        final int seq = 0x11111111;
+        final int ack = 0x22222222;
+        final int wnd = 8000;
+        final int wndScale = 2;
+        final int tos = 4;
+        final int ttl = 64;
+        TcpKeepalivePacketData resultData = null;
+        final TcpKeepalivePacketDataParcelable testInfo = new TcpKeepalivePacketDataParcelable();
+        testInfo.srcAddress = IPV4_KEEPALIVE_SRC_ADDR;
+        testInfo.srcPort = srcPort;
+        testInfo.dstAddress = IPV4_KEEPALIVE_DST_ADDR;
+        testInfo.dstPort = dstPort;
+        testInfo.seq = seq;
+        testInfo.ack = ack;
+        testInfo.rcvWnd = wnd;
+        testInfo.rcvWndScale = wndScale;
+        testInfo.tos = tos;
+        testInfo.ttl = ttl;
+        try {
+            resultData = KeepalivePacketDataUtil.fromStableParcelable(testInfo);
+        } catch (InvalidPacketException e) {
+            fail("InvalidPacketException: " + e);
+        }
+
+        assertEquals(InetAddress.getByAddress(testInfo.srcAddress), resultData.getSrcAddress());
+        assertEquals(InetAddress.getByAddress(testInfo.dstAddress), resultData.getDstAddress());
+        assertEquals(testInfo.srcPort, resultData.getSrcPort());
+        assertEquals(testInfo.dstPort, resultData.getDstPort());
+        assertEquals(testInfo.seq, resultData.tcpSeq);
+        assertEquals(testInfo.ack, resultData.tcpAck);
+        assertEquals(testInfo.rcvWnd, resultData.tcpWindow);
+        assertEquals(testInfo.rcvWndScale, resultData.tcpWindowScale);
+        assertEquals(testInfo.tos, resultData.ipTos);
+        assertEquals(testInfo.ttl, resultData.ipTtl);
+
+        assertParcelingIsLossless(resultData);
+
+        final byte[] packet = resultData.getPacket();
+        // IP version and IHL
+        assertEquals(packet[0], 0x45);
+        // TOS
+        assertEquals(packet[1], tos);
+        // TTL
+        assertEquals(packet[8], ttl);
+        // Source IP address.
+        byte[] ip = new byte[4];
+        ByteBuffer buf = ByteBuffer.wrap(packet, 12, 4);
+        buf.get(ip);
+        assertArrayEquals(ip, IPV4_KEEPALIVE_SRC_ADDR);
+        // Destination IP address.
+        buf = ByteBuffer.wrap(packet, 16, 4);
+        buf.get(ip);
+        assertArrayEquals(ip, IPV4_KEEPALIVE_DST_ADDR);
+
+        buf = ByteBuffer.wrap(packet, 20, 12);
+        // Source port.
+        assertEquals(buf.getShort(), srcPort);
+        // Destination port.
+        assertEquals(buf.getShort(), dstPort);
+        // Sequence number.
+        assertEquals(buf.getInt(), seq);
+        // Ack.
+        assertEquals(buf.getInt(), ack);
+        // Window size.
+        buf = ByteBuffer.wrap(packet, 34, 2);
+        assertEquals(buf.getShort(), wnd >> wndScale);
+    }
+
+    //TODO: add ipv6 test when ipv6 supported
+
+    @Test
+    public void testToTcpKeepaliveStableParcelable() throws Exception {
+        final int srcPort = 1234;
+        final int dstPort = 4321;
+        final int sequence = 0x11111111;
+        final int ack = 0x22222222;
+        final int wnd = 48_000;
+        final int wndScale = 2;
+        final int tos = 4;
+        final int ttl = 64;
+        final TcpKeepalivePacketDataParcelable testInfo = new TcpKeepalivePacketDataParcelable();
+        testInfo.srcAddress = IPV4_KEEPALIVE_SRC_ADDR;
+        testInfo.srcPort = srcPort;
+        testInfo.dstAddress = IPV4_KEEPALIVE_DST_ADDR;
+        testInfo.dstPort = dstPort;
+        testInfo.seq = sequence;
+        testInfo.ack = ack;
+        testInfo.rcvWnd = wnd;
+        testInfo.rcvWndScale = wndScale;
+        testInfo.tos = tos;
+        testInfo.ttl = ttl;
+        TcpKeepalivePacketData testData = null;
+        TcpKeepalivePacketDataParcelable resultData = null;
+        testData = KeepalivePacketDataUtil.fromStableParcelable(testInfo);
+        resultData = KeepalivePacketDataUtil.toStableParcelable(testData);
+        assertArrayEquals(resultData.srcAddress, IPV4_KEEPALIVE_SRC_ADDR);
+        assertArrayEquals(resultData.dstAddress, IPV4_KEEPALIVE_DST_ADDR);
+        assertEquals(resultData.srcPort, srcPort);
+        assertEquals(resultData.dstPort, dstPort);
+        assertEquals(resultData.seq, sequence);
+        assertEquals(resultData.ack, ack);
+        assertEquals(resultData.rcvWnd, wnd);
+        assertEquals(resultData.rcvWndScale, wndScale);
+        assertEquals(resultData.tos, tos);
+        assertEquals(resultData.ttl, ttl);
+
+        final String expected = ""
+                + "android.net.TcpKeepalivePacketDataParcelable{srcAddress: [10, 0, 0, 1],"
+                + " srcPort: 1234, dstAddress: [10, 0, 0, 5], dstPort: 4321, seq: 286331153,"
+                + " ack: 572662306, rcvWnd: 48000, rcvWndScale: 2, tos: 4, ttl: 64}";
+        assertEquals(expected, resultData.toString());
+    }
+
+    @Test
+    public void testParseTcpKeepalivePacketData() throws Exception {
+        final int srcPort = 1234;
+        final int dstPort = 4321;
+        final int sequence = 0x11111111;
+        final int ack = 0x22222222;
+        final int wnd = 4800;
+        final int wndScale = 2;
+        final int tos = 4;
+        final int ttl = 64;
+        final TcpKeepalivePacketDataParcelable testParcel = new TcpKeepalivePacketDataParcelable();
+        testParcel.srcAddress = IPV4_KEEPALIVE_SRC_ADDR;
+        testParcel.srcPort = srcPort;
+        testParcel.dstAddress = IPV4_KEEPALIVE_DST_ADDR;
+        testParcel.dstPort = dstPort;
+        testParcel.seq = sequence;
+        testParcel.ack = ack;
+        testParcel.rcvWnd = wnd;
+        testParcel.rcvWndScale = wndScale;
+        testParcel.tos = tos;
+        testParcel.ttl = ttl;
+
+        final KeepalivePacketData testData =
+                KeepalivePacketDataUtil.fromStableParcelable(testParcel);
+        final TcpKeepalivePacketDataParcelable parsedParcelable =
+                KeepalivePacketDataUtil.parseTcpKeepalivePacketData(testData);
+        final TcpKeepalivePacketData roundTripData =
+                KeepalivePacketDataUtil.fromStableParcelable(parsedParcelable);
+
+        // Generated packet is the same, but rcvWnd / wndScale will differ if scale is non-zero
+        assertTrue(testData.getPacket().length > 0);
+        assertArrayEquals(testData.getPacket(), roundTripData.getPacket());
+
+        testParcel.rcvWndScale = 0;
+        final KeepalivePacketData noScaleTestData =
+                KeepalivePacketDataUtil.fromStableParcelable(testParcel);
+        final TcpKeepalivePacketDataParcelable noScaleParsedParcelable =
+                KeepalivePacketDataUtil.parseTcpKeepalivePacketData(noScaleTestData);
+        final TcpKeepalivePacketData noScaleRoundTripData =
+                KeepalivePacketDataUtil.fromStableParcelable(noScaleParsedParcelable);
+        assertEquals(noScaleTestData, noScaleRoundTripData);
+        assertTrue(noScaleTestData.getPacket().length > 0);
+        assertArrayEquals(noScaleTestData.getPacket(), noScaleRoundTripData.getPacket());
+    }
+}
diff --git a/tests/unit/java/android/net/MacAddressTest.java b/tests/unit/java/android/net/MacAddressTest.java
new file mode 100644
index 0000000..6de31f6
--- /dev/null
+++ b/tests/unit/java/android/net/MacAddressTest.java
@@ -0,0 +1,312 @@
+/*
+ * 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 static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.MacAddressUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.util.Arrays;
+import java.util.Random;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MacAddressTest {
+
+    static class AddrTypeTestCase {
+        byte[] addr;
+        int expectedType;
+
+        static AddrTypeTestCase of(int expectedType, int... addr) {
+            AddrTypeTestCase t = new AddrTypeTestCase();
+            t.expectedType = expectedType;
+            t.addr = toByteArray(addr);
+            return t;
+        }
+    }
+
+    @Test
+    public void testMacAddrTypes() {
+        AddrTypeTestCase[] testcases = {
+            AddrTypeTestCase.of(MacAddress.TYPE_UNKNOWN),
+            AddrTypeTestCase.of(MacAddress.TYPE_UNKNOWN, 0),
+            AddrTypeTestCase.of(MacAddress.TYPE_UNKNOWN, 1, 2, 3, 4, 5),
+            AddrTypeTestCase.of(MacAddress.TYPE_UNKNOWN, 1, 2, 3, 4, 5, 6, 7),
+            AddrTypeTestCase.of(MacAddress.TYPE_UNICAST, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0),
+            AddrTypeTestCase.of(MacAddress.TYPE_BROADCAST, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff),
+            AddrTypeTestCase.of(MacAddress.TYPE_MULTICAST, 1, 2, 3, 4, 5, 6),
+            AddrTypeTestCase.of(MacAddress.TYPE_MULTICAST, 11, 22, 33, 44, 55, 66),
+            AddrTypeTestCase.of(MacAddress.TYPE_MULTICAST, 33, 33, 0xaa, 0xbb, 0xcc, 0xdd)
+        };
+
+        for (AddrTypeTestCase t : testcases) {
+            int got = MacAddress.macAddressType(t.addr);
+            String msg = String.format("expected type of %s to be %s, but got %s",
+                    Arrays.toString(t.addr), t.expectedType, got);
+            assertEquals(msg, t.expectedType, got);
+
+            if (got != MacAddress.TYPE_UNKNOWN) {
+                assertEquals(got, MacAddress.fromBytes(t.addr).getAddressType());
+            }
+        }
+    }
+
+    @Test
+    public void testToOuiString() {
+        String[][] macs = {
+            {"07:00:d3:56:8a:c4", "07:00:d3"},
+            {"33:33:aa:bb:cc:dd", "33:33:aa"},
+            {"06:00:00:00:00:00", "06:00:00"},
+            {"07:00:d3:56:8a:c4", "07:00:d3"}
+        };
+
+        for (String[] pair : macs) {
+            String mac = pair[0];
+            String expected = pair[1];
+            assertEquals(expected, MacAddress.fromString(mac).toOuiString());
+        }
+    }
+
+    @Test
+    public void testHexPaddingWhenPrinting() {
+        String[] macs = {
+            "07:00:d3:56:8a:c4",
+            "33:33:aa:bb:cc:dd",
+            "06:00:00:00:00:00",
+            "07:00:d3:56:8a:c4"
+        };
+
+        for (String mac : macs) {
+            assertEquals(mac, MacAddress.fromString(mac).toString());
+            assertEquals(mac,
+                    MacAddress.stringAddrFromByteAddr(MacAddress.byteAddrFromStringAddr(mac)));
+        }
+    }
+
+    @Test
+    public void testIsMulticastAddress() {
+        MacAddress[] multicastAddresses = {
+            MacAddress.BROADCAST_ADDRESS,
+            MacAddress.fromString("07:00:d3:56:8a:c4"),
+            MacAddress.fromString("33:33:aa:bb:cc:dd"),
+        };
+        MacAddress[] unicastAddresses = {
+            MacAddress.ALL_ZEROS_ADDRESS,
+            MacAddress.fromString("00:01:44:55:66:77"),
+            MacAddress.fromString("08:00:22:33:44:55"),
+            MacAddress.fromString("06:00:00:00:00:00"),
+        };
+
+        for (MacAddress mac : multicastAddresses) {
+            String msg = mac.toString() + " expected to be a multicast address";
+            assertTrue(msg, MacAddressUtils.isMulticastAddress(mac));
+        }
+        for (MacAddress mac : unicastAddresses) {
+            String msg = mac.toString() + " expected not to be a multicast address";
+            assertFalse(msg, MacAddressUtils.isMulticastAddress(mac));
+        }
+    }
+
+    @Test
+    public void testIsLocallyAssignedAddress() {
+        MacAddress[] localAddresses = {
+            MacAddress.fromString("06:00:00:00:00:00"),
+            MacAddress.fromString("07:00:d3:56:8a:c4"),
+            MacAddress.fromString("33:33:aa:bb:cc:dd"),
+        };
+        MacAddress[] universalAddresses = {
+            MacAddress.fromString("00:01:44:55:66:77"),
+            MacAddress.fromString("08:00:22:33:44:55"),
+        };
+
+        for (MacAddress mac : localAddresses) {
+            String msg = mac.toString() + " expected to be a locally assigned address";
+            assertTrue(msg, mac.isLocallyAssigned());
+        }
+        for (MacAddress mac : universalAddresses) {
+            String msg = mac.toString() + " expected not to be globally unique address";
+            assertFalse(msg, mac.isLocallyAssigned());
+        }
+    }
+
+    @Test
+    public void testMacAddressConversions() {
+        final int iterations = 10000;
+        for (int i = 0; i < iterations; i++) {
+            MacAddress mac = MacAddressUtils.createRandomUnicastAddress();
+
+            String stringRepr = mac.toString();
+            byte[] bytesRepr = mac.toByteArray();
+
+            assertEquals(mac, MacAddress.fromString(stringRepr));
+            assertEquals(mac, MacAddress.fromBytes(bytesRepr));
+
+            assertEquals(mac, MacAddress.fromString(MacAddress.stringAddrFromByteAddr(bytesRepr)));
+            assertEquals(mac, MacAddress.fromBytes(MacAddress.byteAddrFromStringAddr(stringRepr)));
+        }
+    }
+
+    @Test
+    public void testMacAddressRandomGeneration() {
+        final int iterations = 1000;
+        final String expectedAndroidOui = "da:a1:19";
+        for (int i = 0; i < iterations; i++) {
+            MacAddress mac = MacAddress.createRandomUnicastAddressWithGoogleBase();
+            String stringRepr = mac.toString();
+
+            assertTrue(stringRepr + " expected to be a locally assigned address",
+                    mac.isLocallyAssigned());
+            assertTrue(stringRepr + " expected to begin with " + expectedAndroidOui,
+                    stringRepr.startsWith(expectedAndroidOui));
+        }
+
+        final Random r = new Random();
+        final String anotherOui = "24:5f:78";
+        final String expectedLocalOui = "26:5f:78";
+        final MacAddress base = MacAddress.fromString(anotherOui + ":0:0:0");
+        for (int i = 0; i < iterations; i++) {
+            MacAddress mac = MacAddressUtils.createRandomUnicastAddress(base, r);
+            String stringRepr = mac.toString();
+
+            assertTrue(stringRepr + " expected to be a locally assigned address",
+                    mac.isLocallyAssigned());
+            assertEquals(MacAddress.TYPE_UNICAST, mac.getAddressType());
+            assertTrue(stringRepr + " expected to begin with " + expectedLocalOui,
+                    stringRepr.startsWith(expectedLocalOui));
+        }
+
+        for (int i = 0; i < iterations; i++) {
+            MacAddress mac = MacAddressUtils.createRandomUnicastAddress();
+            String stringRepr = mac.toString();
+
+            assertTrue(stringRepr + " expected to be a locally assigned address",
+                    mac.isLocallyAssigned());
+            assertEquals(MacAddress.TYPE_UNICAST, mac.getAddressType());
+        }
+    }
+
+    @Test
+    public void testConstructorInputValidation() {
+        String[] invalidStringAddresses = {
+            "",
+            "abcd",
+            "1:2:3:4:5",
+            "1:2:3:4:5:6:7",
+            "10000:2:3:4:5:6",
+        };
+
+        for (String s : invalidStringAddresses) {
+            try {
+                MacAddress mac = MacAddress.fromString(s);
+                fail("MacAddress.fromString(" + s + ") should have failed, but returned " + mac);
+            } catch (IllegalArgumentException excepted) {
+            }
+        }
+
+        try {
+            MacAddress mac = MacAddress.fromString(null);
+            fail("MacAddress.fromString(null) should have failed, but returned " + mac);
+        } catch (NullPointerException excepted) {
+        }
+
+        byte[][] invalidBytesAddresses = {
+            {},
+            {1,2,3,4,5},
+            {1,2,3,4,5,6,7},
+        };
+
+        for (byte[] b : invalidBytesAddresses) {
+            try {
+                MacAddress mac = MacAddress.fromBytes(b);
+                fail("MacAddress.fromBytes(" + Arrays.toString(b)
+                        + ") should have failed, but returned " + mac);
+            } catch (IllegalArgumentException excepted) {
+            }
+        }
+
+        try {
+            MacAddress mac = MacAddress.fromBytes(null);
+            fail("MacAddress.fromBytes(null) should have failed, but returned " + mac);
+        } catch (NullPointerException excepted) {
+        }
+    }
+
+    @Test
+    public void testMatches() {
+        // match 4 bytes prefix
+        assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+                MacAddress.fromString("aa:bb:cc:dd:00:00"),
+                MacAddress.fromString("ff:ff:ff:ff:00:00")));
+
+        // match bytes 0,1,2 and 5
+        assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+                MacAddress.fromString("aa:bb:cc:00:00:11"),
+                MacAddress.fromString("ff:ff:ff:00:00:ff")));
+
+        // match 34 bit prefix
+        assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+                MacAddress.fromString("aa:bb:cc:dd:c0:00"),
+                MacAddress.fromString("ff:ff:ff:ff:c0:00")));
+
+        // fail to match 36 bit prefix
+        assertFalse(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+                MacAddress.fromString("aa:bb:cc:dd:40:00"),
+                MacAddress.fromString("ff:ff:ff:ff:f0:00")));
+
+        // match all 6 bytes
+        assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+                MacAddress.fromString("aa:bb:cc:dd:ee:11"),
+                MacAddress.fromString("ff:ff:ff:ff:ff:ff")));
+
+        // match none of 6 bytes
+        assertTrue(MacAddress.fromString("aa:bb:cc:dd:ee:11").matches(
+                MacAddress.fromString("00:00:00:00:00:00"),
+                MacAddress.fromString("00:00:00:00:00:00")));
+    }
+
+    /**
+     * Tests that link-local address generation from MAC is valid.
+     */
+    @Test
+    public void testLinkLocalFromMacGeneration() {
+        MacAddress mac = MacAddress.fromString("52:74:f2:b1:a8:7f");
+        byte[] inet6ll = {(byte) 0xfe, (byte) 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x74,
+            (byte) 0xf2, (byte) 0xff, (byte) 0xfe, (byte) 0xb1, (byte) 0xa8, 0x7f};
+        Inet6Address llv6 = mac.getLinkLocalIpv6FromEui48Mac();
+        assertTrue(llv6.isLinkLocalAddress());
+        assertArrayEquals(inet6ll, llv6.getAddress());
+    }
+
+    static byte[] toByteArray(int... in) {
+        byte[] out = new byte[in.length];
+        for (int i = 0; i < in.length; i++) {
+            out[i] = (byte) in[i];
+        }
+        return out;
+    }
+}
diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt
new file mode 100644
index 0000000..eb2b85c
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkIdentityTest.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.NetworkIdentity.OEM_NONE
+import android.net.NetworkIdentity.OEM_PAID
+import android.net.NetworkIdentity.OEM_PRIVATE
+import android.net.NetworkIdentity.getOemBitfield
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+
+@RunWith(JUnit4::class)
+class NetworkIdentityTest {
+    @Test
+    fun testGetOemBitfield() {
+        val oemNone = NetworkCapabilities().apply {
+            setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, false)
+            setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, false)
+        }
+        val oemPaid = NetworkCapabilities().apply {
+            setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, true)
+            setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, false)
+        }
+        val oemPrivate = NetworkCapabilities().apply {
+            setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, false)
+            setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, true)
+        }
+        val oemAll = NetworkCapabilities().apply {
+            setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, true)
+            setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, true)
+        }
+
+        assertEquals(getOemBitfield(oemNone), OEM_NONE)
+        assertEquals(getOemBitfield(oemPaid), OEM_PAID)
+        assertEquals(getOemBitfield(oemPrivate), OEM_PRIVATE)
+        assertEquals(getOemBitfield(oemAll), OEM_PAID or OEM_PRIVATE)
+    }
+}
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
new file mode 100644
index 0000000..13558cd
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -0,0 +1,599 @@
+/*
+ * 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.NetworkStatsHistory.DataStreamUtils.readVarLong;
+import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLong;
+import static android.net.NetworkStatsHistory.Entry.UNKNOWN;
+import static android.net.NetworkStatsHistory.FIELD_ALL;
+import static android.net.NetworkStatsHistory.FIELD_OPERATIONS;
+import static android.net.NetworkStatsHistory.FIELD_RX_BYTES;
+import static android.net.NetworkStatsHistory.FIELD_RX_PACKETS;
+import static android.net.NetworkStatsHistory.FIELD_TX_BYTES;
+import static android.net.TrafficStats.GB_IN_BYTES;
+import static android.net.TrafficStats.MB_IN_BYTES;
+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 android.text.format.DateUtils.WEEK_IN_MILLIS;
+import static android.text.format.DateUtils.YEAR_IN_MILLIS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.frameworks.tests.net.R;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.util.Random;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkStatsHistoryTest {
+    private static final String TAG = "NetworkStatsHistoryTest";
+
+    private static final long TEST_START = 1194220800000L;
+
+    private NetworkStatsHistory stats;
+
+    @After
+    public void tearDown() throws Exception {
+        if (stats != null) {
+            assertConsistent(stats);
+        }
+    }
+
+    @Test
+    public void testReadOriginalVersion() throws Exception {
+        final Context context = InstrumentationRegistry.getContext();
+        final DataInputStream in =
+                new DataInputStream(context.getResources().openRawResource(R.raw.history_v1));
+
+        NetworkStatsHistory.Entry entry = null;
+        try {
+            final NetworkStatsHistory history = new NetworkStatsHistory(in);
+            assertEquals(15 * SECOND_IN_MILLIS, history.getBucketDuration());
+
+            entry = history.getValues(0, entry);
+            assertEquals(29143L, entry.rxBytes);
+            assertEquals(6223L, entry.txBytes);
+
+            entry = history.getValues(history.size() - 1, entry);
+            assertEquals(1476L, entry.rxBytes);
+            assertEquals(838L, entry.txBytes);
+
+            entry = history.getValues(Long.MIN_VALUE, Long.MAX_VALUE, entry);
+            assertEquals(332401L, entry.rxBytes);
+            assertEquals(64314L, entry.txBytes);
+
+        } finally {
+            in.close();
+        }
+    }
+
+    @Test
+    public void testRecordSingleBucket() throws Exception {
+        final long BUCKET_SIZE = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        // record data into narrow window to get single bucket
+        stats.recordData(TEST_START, TEST_START + SECOND_IN_MILLIS,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+
+        assertEquals(1, stats.size());
+        assertValues(stats, 0, SECOND_IN_MILLIS, 1024L, 10L, 2048L, 20L, 2L);
+    }
+
+    @Test
+    public void testRecordEqualBuckets() throws Exception {
+        final long bucketDuration = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(bucketDuration);
+
+        // split equally across two buckets
+        final long recordStart = TEST_START + (bucketDuration / 2);
+        stats.recordData(recordStart, recordStart + bucketDuration,
+                new NetworkStats.Entry(1024L, 10L, 128L, 2L, 2L));
+
+        assertEquals(2, stats.size());
+        assertValues(stats, 0, HOUR_IN_MILLIS / 2, 512L, 5L, 64L, 1L, 1L);
+        assertValues(stats, 1, HOUR_IN_MILLIS / 2, 512L, 5L, 64L, 1L, 1L);
+    }
+
+    @Test
+    public void testRecordTouchingBuckets() throws Exception {
+        final long BUCKET_SIZE = 15 * MINUTE_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        // split almost completely into middle bucket, but with a few minutes
+        // overlap into neighboring buckets. total record is 20 minutes.
+        final long recordStart = (TEST_START + BUCKET_SIZE) - MINUTE_IN_MILLIS;
+        final long recordEnd = (TEST_START + (BUCKET_SIZE * 2)) + (MINUTE_IN_MILLIS * 4);
+        stats.recordData(recordStart, recordEnd,
+                new NetworkStats.Entry(1000L, 2000L, 5000L, 10000L, 100L));
+
+        assertEquals(3, stats.size());
+        // first bucket should have (1/20 of value)
+        assertValues(stats, 0, MINUTE_IN_MILLIS, 50L, 100L, 250L, 500L, 5L);
+        // second bucket should have (15/20 of value)
+        assertValues(stats, 1, 15 * MINUTE_IN_MILLIS, 750L, 1500L, 3750L, 7500L, 75L);
+        // final bucket should have (4/20 of value)
+        assertValues(stats, 2, 4 * MINUTE_IN_MILLIS, 200L, 400L, 1000L, 2000L, 20L);
+    }
+
+    @Test
+    public void testRecordGapBuckets() throws Exception {
+        final long BUCKET_SIZE = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        // record some data today and next week with large gap
+        final long firstStart = TEST_START;
+        final long lastStart = TEST_START + WEEK_IN_MILLIS;
+        stats.recordData(firstStart, firstStart + SECOND_IN_MILLIS,
+                new NetworkStats.Entry(128L, 2L, 256L, 4L, 1L));
+        stats.recordData(lastStart, lastStart + SECOND_IN_MILLIS,
+                new NetworkStats.Entry(64L, 1L, 512L, 8L, 2L));
+
+        // we should have two buckets, far apart from each other
+        assertEquals(2, stats.size());
+        assertValues(stats, 0, SECOND_IN_MILLIS, 128L, 2L, 256L, 4L, 1L);
+        assertValues(stats, 1, SECOND_IN_MILLIS, 64L, 1L, 512L, 8L, 2L);
+
+        // now record something in middle, spread across two buckets
+        final long middleStart = TEST_START + DAY_IN_MILLIS;
+        final long middleEnd = middleStart + (HOUR_IN_MILLIS * 2);
+        stats.recordData(middleStart, middleEnd,
+                new NetworkStats.Entry(2048L, 4L, 2048L, 4L, 2L));
+
+        // now should have four buckets, with new record in middle two buckets
+        assertEquals(4, stats.size());
+        assertValues(stats, 0, SECOND_IN_MILLIS, 128L, 2L, 256L, 4L, 1L);
+        assertValues(stats, 1, HOUR_IN_MILLIS, 1024L, 2L, 1024L, 2L, 1L);
+        assertValues(stats, 2, HOUR_IN_MILLIS, 1024L, 2L, 1024L, 2L, 1L);
+        assertValues(stats, 3, SECOND_IN_MILLIS, 64L, 1L, 512L, 8L, 2L);
+    }
+
+    @Test
+    public void testRecordOverlapBuckets() throws Exception {
+        final long BUCKET_SIZE = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        // record some data in one bucket, and another overlapping buckets
+        stats.recordData(TEST_START, TEST_START + SECOND_IN_MILLIS,
+                new NetworkStats.Entry(256L, 2L, 256L, 2L, 1L));
+        final long midStart = TEST_START + (HOUR_IN_MILLIS / 2);
+        stats.recordData(midStart, midStart + HOUR_IN_MILLIS,
+                new NetworkStats.Entry(1024L, 10L, 1024L, 10L, 10L));
+
+        // should have two buckets, with some data mixed together
+        assertEquals(2, stats.size());
+        assertValues(stats, 0, SECOND_IN_MILLIS + (HOUR_IN_MILLIS / 2), 768L, 7L, 768L, 7L, 6L);
+        assertValues(stats, 1, (HOUR_IN_MILLIS / 2), 512L, 5L, 512L, 5L, 5L);
+    }
+
+    @Test
+    public void testRecordEntireGapIdentical() throws Exception {
+        // first, create two separate histories far apart
+        final NetworkStatsHistory stats1 = new NetworkStatsHistory(HOUR_IN_MILLIS);
+        stats1.recordData(TEST_START, TEST_START + 2 * HOUR_IN_MILLIS, 2000L, 1000L);
+
+        final long TEST_START_2 = TEST_START + DAY_IN_MILLIS;
+        final NetworkStatsHistory stats2 = new NetworkStatsHistory(HOUR_IN_MILLIS);
+        stats2.recordData(TEST_START_2, TEST_START_2 + 2 * HOUR_IN_MILLIS, 1000L, 500L);
+
+        // combine together with identical bucket size
+        stats = new NetworkStatsHistory(HOUR_IN_MILLIS);
+        stats.recordEntireHistory(stats1);
+        stats.recordEntireHistory(stats2);
+
+        // first verify that totals match up
+        assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 3000L, 1500L);
+
+        // now inspect internal buckets
+        assertValues(stats, 0, 1000L, 500L);
+        assertValues(stats, 1, 1000L, 500L);
+        assertValues(stats, 2, 500L, 250L);
+        assertValues(stats, 3, 500L, 250L);
+    }
+
+    @Test
+    public void testRecordEntireOverlapVaryingBuckets() throws Exception {
+        // create history just over hour bucket boundary
+        final NetworkStatsHistory stats1 = new NetworkStatsHistory(HOUR_IN_MILLIS);
+        stats1.recordData(TEST_START, TEST_START + MINUTE_IN_MILLIS * 60, 600L, 600L);
+
+        final long TEST_START_2 = TEST_START + MINUTE_IN_MILLIS;
+        final NetworkStatsHistory stats2 = new NetworkStatsHistory(MINUTE_IN_MILLIS);
+        stats2.recordData(TEST_START_2, TEST_START_2 + MINUTE_IN_MILLIS * 5, 50L, 50L);
+
+        // combine together with minute bucket size
+        stats = new NetworkStatsHistory(MINUTE_IN_MILLIS);
+        stats.recordEntireHistory(stats1);
+        stats.recordEntireHistory(stats2);
+
+        // first verify that totals match up
+        assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 650L, 650L);
+
+        // now inspect internal buckets
+        assertValues(stats, 0, 10L, 10L);
+        assertValues(stats, 1, 20L, 20L);
+        assertValues(stats, 2, 20L, 20L);
+        assertValues(stats, 3, 20L, 20L);
+        assertValues(stats, 4, 20L, 20L);
+        assertValues(stats, 5, 20L, 20L);
+        assertValues(stats, 6, 10L, 10L);
+
+        // now combine using 15min buckets
+        stats = new NetworkStatsHistory(HOUR_IN_MILLIS / 4);
+        stats.recordEntireHistory(stats1);
+        stats.recordEntireHistory(stats2);
+
+        // first verify that totals match up
+        assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 650L, 650L);
+
+        // and inspect buckets
+        assertValues(stats, 0, 200L, 200L);
+        assertValues(stats, 1, 150L, 150L);
+        assertValues(stats, 2, 150L, 150L);
+        assertValues(stats, 3, 150L, 150L);
+    }
+
+    @Test
+    public void testRemove() throws Exception {
+        stats = new NetworkStatsHistory(HOUR_IN_MILLIS);
+
+        // record some data across 24 buckets
+        stats.recordData(TEST_START, TEST_START + DAY_IN_MILLIS, 24L, 24L);
+        assertEquals(24, stats.size());
+
+        // try removing invalid data; should be no change
+        stats.removeBucketsBefore(0 - DAY_IN_MILLIS);
+        assertEquals(24, stats.size());
+
+        // try removing far before buckets; should be no change
+        stats.removeBucketsBefore(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);
+        assertEquals(24, stats.size());
+
+        // try removing single bucket
+        stats.removeBucketsBefore(TEST_START + HOUR_IN_MILLIS);
+        assertEquals(23, stats.size());
+
+        // try removing multiple buckets
+        stats.removeBucketsBefore(TEST_START + (4 * HOUR_IN_MILLIS));
+        assertEquals(20, stats.size());
+
+        // try removing all buckets
+        stats.removeBucketsBefore(TEST_START + YEAR_IN_MILLIS);
+        assertEquals(0, stats.size());
+    }
+
+    @Test
+    public void testTotalData() throws Exception {
+        final long BUCKET_SIZE = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        // record uniform data across day
+        stats.recordData(TEST_START, TEST_START + DAY_IN_MILLIS, 2400L, 4800L);
+
+        // verify that total outside range is 0
+        assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START - DAY_IN_MILLIS, 0L, 0L);
+
+        // verify total in first hour
+        assertValues(stats, TEST_START, TEST_START + HOUR_IN_MILLIS, 100L, 200L);
+
+        // verify total across 1.5 hours
+        assertValues(stats, TEST_START, TEST_START + (long) (1.5 * HOUR_IN_MILLIS), 150L, 300L);
+
+        // verify total beyond end
+        assertValues(stats, TEST_START + (23 * HOUR_IN_MILLIS), TEST_START + WEEK_IN_MILLIS, 100L, 200L);
+
+        // verify everything total
+        assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 2400L, 4800L);
+
+    }
+
+    @Test
+    public void testFuzzing() throws Exception {
+        try {
+            // fuzzing with random events, looking for crashes
+            final NetworkStats.Entry entry = new NetworkStats.Entry();
+            final Random r = new Random();
+            for (int i = 0; i < 500; i++) {
+                stats = new NetworkStatsHistory(r.nextLong());
+                for (int j = 0; j < 10000; j++) {
+                    if (r.nextBoolean()) {
+                        // add range
+                        final long start = r.nextLong();
+                        final long end = start + r.nextInt();
+                        entry.rxBytes = nextPositiveLong(r);
+                        entry.rxPackets = nextPositiveLong(r);
+                        entry.txBytes = nextPositiveLong(r);
+                        entry.txPackets = nextPositiveLong(r);
+                        entry.operations = nextPositiveLong(r);
+                        stats.recordData(start, end, entry);
+                    } else {
+                        // trim something
+                        stats.removeBucketsBefore(r.nextLong());
+                    }
+                }
+                assertConsistent(stats);
+            }
+        } catch (Throwable e) {
+            Log.e(TAG, String.valueOf(stats));
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static long nextPositiveLong(Random r) {
+        final long value = r.nextLong();
+        return value < 0 ? -value : value;
+    }
+
+    @Test
+    public void testIgnoreFields() throws Exception {
+        final NetworkStatsHistory history = new NetworkStatsHistory(
+                MINUTE_IN_MILLIS, 0, FIELD_RX_BYTES | FIELD_TX_BYTES);
+
+        history.recordData(0, MINUTE_IN_MILLIS,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L));
+        history.recordData(0, 2 * MINUTE_IN_MILLIS,
+                new NetworkStats.Entry(2L, 2L, 2L, 2L, 2L));
+
+        assertFullValues(history, UNKNOWN, 1026L, UNKNOWN, 2050L, UNKNOWN, UNKNOWN);
+    }
+
+    @Test
+    public void testIgnoreFieldsRecordIn() throws Exception {
+        final NetworkStatsHistory full = new NetworkStatsHistory(MINUTE_IN_MILLIS, 0, FIELD_ALL);
+        final NetworkStatsHistory partial = new NetworkStatsHistory(
+                MINUTE_IN_MILLIS, 0, FIELD_RX_PACKETS | FIELD_OPERATIONS);
+
+        full.recordData(0, MINUTE_IN_MILLIS,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L));
+        partial.recordEntireHistory(full);
+
+        assertFullValues(partial, UNKNOWN, UNKNOWN, 10L, UNKNOWN, UNKNOWN, 4L);
+    }
+
+    @Test
+    public void testIgnoreFieldsRecordOut() throws Exception {
+        final NetworkStatsHistory full = new NetworkStatsHistory(MINUTE_IN_MILLIS, 0, FIELD_ALL);
+        final NetworkStatsHistory partial = new NetworkStatsHistory(
+                MINUTE_IN_MILLIS, 0, FIELD_RX_PACKETS | FIELD_OPERATIONS);
+
+        partial.recordData(0, MINUTE_IN_MILLIS,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L));
+        full.recordEntireHistory(partial);
+
+        assertFullValues(full, MINUTE_IN_MILLIS, 0L, 10L, 0L, 0L, 4L);
+    }
+
+    @Test
+    public void testSerialize() throws Exception {
+        final NetworkStatsHistory before = new NetworkStatsHistory(MINUTE_IN_MILLIS, 40, FIELD_ALL);
+        before.recordData(0, 4 * MINUTE_IN_MILLIS,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L));
+        before.recordData(DAY_IN_MILLIS, DAY_IN_MILLIS + MINUTE_IN_MILLIS,
+                new NetworkStats.Entry(10L, 20L, 30L, 40L, 50L));
+
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        before.writeToStream(new DataOutputStream(out));
+        out.close();
+
+        final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+        final NetworkStatsHistory after = new NetworkStatsHistory(new DataInputStream(in));
+
+        // must have identical totals before and after
+        assertFullValues(before, 5 * MINUTE_IN_MILLIS, 1034L, 30L, 2078L, 60L, 54L);
+        assertFullValues(after, 5 * MINUTE_IN_MILLIS, 1034L, 30L, 2078L, 60L, 54L);
+    }
+
+    @Test
+    public void testVarLong() throws Exception {
+        assertEquals(0L, performVarLong(0L));
+        assertEquals(-1L, performVarLong(-1L));
+        assertEquals(1024L, performVarLong(1024L));
+        assertEquals(-1024L, performVarLong(-1024L));
+        assertEquals(40 * MB_IN_BYTES, performVarLong(40 * MB_IN_BYTES));
+        assertEquals(512 * GB_IN_BYTES, performVarLong(512 * GB_IN_BYTES));
+        assertEquals(Long.MIN_VALUE, performVarLong(Long.MIN_VALUE));
+        assertEquals(Long.MAX_VALUE, performVarLong(Long.MAX_VALUE));
+        assertEquals(Long.MIN_VALUE + 40, performVarLong(Long.MIN_VALUE + 40));
+        assertEquals(Long.MAX_VALUE - 40, performVarLong(Long.MAX_VALUE - 40));
+    }
+
+    @Test
+    public void testIndexBeforeAfter() throws Exception {
+        final long BUCKET_SIZE = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        final long FIRST_START = TEST_START;
+        final long FIRST_END = FIRST_START + (2 * HOUR_IN_MILLIS);
+        final long SECOND_START = TEST_START + WEEK_IN_MILLIS;
+        final long SECOND_END = SECOND_START + HOUR_IN_MILLIS;
+        final long THIRD_START = TEST_START + (2 * WEEK_IN_MILLIS);
+        final long THIRD_END = THIRD_START + (2 * HOUR_IN_MILLIS);
+
+        stats.recordData(FIRST_START, FIRST_END,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+        stats.recordData(SECOND_START, SECOND_END,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+        stats.recordData(THIRD_START, THIRD_END,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+
+        // should have buckets: 2+1+2
+        assertEquals(5, stats.size());
+
+        assertIndexBeforeAfter(stats, 0, 0, Long.MIN_VALUE);
+        assertIndexBeforeAfter(stats, 0, 1, FIRST_START);
+        assertIndexBeforeAfter(stats, 0, 1, FIRST_START + MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 0, 2, FIRST_START + HOUR_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 1, 2, FIRST_START + HOUR_IN_MILLIS + MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 1, 2, FIRST_END - MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 1, 2, FIRST_END);
+        assertIndexBeforeAfter(stats, 1, 2, FIRST_END + MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 1, 2, SECOND_START - MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 1, 3, SECOND_START);
+        assertIndexBeforeAfter(stats, 2, 3, SECOND_END);
+        assertIndexBeforeAfter(stats, 2, 3, SECOND_END + MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 2, 3, THIRD_START - MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 2, 4, THIRD_START);
+        assertIndexBeforeAfter(stats, 3, 4, THIRD_START + MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 3, 4, THIRD_START + HOUR_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 4, 4, THIRD_END);
+        assertIndexBeforeAfter(stats, 4, 4, THIRD_END + MINUTE_IN_MILLIS);
+        assertIndexBeforeAfter(stats, 4, 4, Long.MAX_VALUE);
+    }
+
+    @Test
+    public void testIntersects() throws Exception {
+        final long BUCKET_SIZE = HOUR_IN_MILLIS;
+        stats = new NetworkStatsHistory(BUCKET_SIZE);
+
+        final long FIRST_START = TEST_START;
+        final long FIRST_END = FIRST_START + (2 * HOUR_IN_MILLIS);
+        final long SECOND_START = TEST_START + WEEK_IN_MILLIS;
+        final long SECOND_END = SECOND_START + HOUR_IN_MILLIS;
+        final long THIRD_START = TEST_START + (2 * WEEK_IN_MILLIS);
+        final long THIRD_END = THIRD_START + (2 * HOUR_IN_MILLIS);
+
+        stats.recordData(FIRST_START, FIRST_END,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+        stats.recordData(SECOND_START, SECOND_END,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+        stats.recordData(THIRD_START, THIRD_END,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+
+        assertFalse(stats.intersects(10, 20));
+        assertFalse(stats.intersects(TEST_START + YEAR_IN_MILLIS, TEST_START + YEAR_IN_MILLIS + 1));
+        assertFalse(stats.intersects(Long.MAX_VALUE, Long.MIN_VALUE));
+
+        assertTrue(stats.intersects(Long.MIN_VALUE, Long.MAX_VALUE));
+        assertTrue(stats.intersects(10, TEST_START + YEAR_IN_MILLIS));
+        assertTrue(stats.intersects(TEST_START, TEST_START));
+        assertTrue(stats.intersects(TEST_START + DAY_IN_MILLIS, TEST_START + DAY_IN_MILLIS + 1));
+        assertTrue(stats.intersects(TEST_START + DAY_IN_MILLIS, Long.MAX_VALUE));
+        assertTrue(stats.intersects(TEST_START + 1, Long.MAX_VALUE));
+
+        assertFalse(stats.intersects(Long.MIN_VALUE, TEST_START - 1));
+        assertTrue(stats.intersects(Long.MIN_VALUE, TEST_START));
+        assertTrue(stats.intersects(Long.MIN_VALUE, TEST_START + 1));
+    }
+
+    @Test
+    public void testSetValues() throws Exception {
+        stats = new NetworkStatsHistory(HOUR_IN_MILLIS);
+        stats.recordData(TEST_START, TEST_START + 1,
+                new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L));
+
+        assertEquals(1024L + 2048L, stats.getTotalBytes());
+
+        final NetworkStatsHistory.Entry entry = stats.getValues(0, null);
+        entry.rxBytes /= 2;
+        entry.txBytes *= 2;
+        stats.setValues(0, entry);
+
+        assertEquals(512L + 4096L, stats.getTotalBytes());
+    }
+
+    private static void assertIndexBeforeAfter(
+            NetworkStatsHistory stats, int before, int after, long time) {
+        assertEquals("unexpected before", before, stats.getIndexBefore(time));
+        assertEquals("unexpected after", after, stats.getIndexAfter(time));
+    }
+
+    private static long performVarLong(long before) throws Exception {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        writeVarLong(new DataOutputStream(out), before);
+
+        final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+        return readVarLong(new DataInputStream(in));
+    }
+
+    private static void assertConsistent(NetworkStatsHistory stats) {
+        // verify timestamps are monotonic
+        long lastStart = Long.MIN_VALUE;
+        NetworkStatsHistory.Entry entry = null;
+        for (int i = 0; i < stats.size(); i++) {
+            entry = stats.getValues(i, entry);
+            assertTrue(lastStart < entry.bucketStart);
+            lastStart = entry.bucketStart;
+        }
+    }
+
+    private static void assertValues(
+            NetworkStatsHistory stats, int index, long rxBytes, long txBytes) {
+        final NetworkStatsHistory.Entry entry = stats.getValues(index, null);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+    }
+
+    private static void assertValues(
+            NetworkStatsHistory stats, long start, long end, long rxBytes, long txBytes) {
+        final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+    }
+
+    private static void assertValues(NetworkStatsHistory stats, int index, long activeTime,
+            long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) {
+        final NetworkStatsHistory.Entry entry = stats.getValues(index, null);
+        assertEquals("unexpected activeTime", activeTime, entry.activeTime);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+        assertEquals("unexpected txPackets", txPackets, entry.txPackets);
+        assertEquals("unexpected operations", operations, entry.operations);
+    }
+
+    private static void assertFullValues(NetworkStatsHistory stats, long activeTime, long rxBytes,
+            long rxPackets, long txBytes, long txPackets, long operations) {
+        assertValues(stats, Long.MIN_VALUE, Long.MAX_VALUE, activeTime, rxBytes, rxPackets, txBytes,
+                txPackets, operations);
+    }
+
+    private static void assertValues(NetworkStatsHistory stats, long start, long end,
+            long activeTime, long rxBytes, long rxPackets, long txBytes, long txPackets,
+            long operations) {
+        final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null);
+        assertEquals("unexpected activeTime", activeTime, entry.activeTime);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+        assertEquals("unexpected txPackets", txPackets, entry.txPackets);
+        assertEquals("unexpected operations", operations, entry.operations);
+    }
+}
diff --git a/tests/unit/java/android/net/NetworkStatsTest.java b/tests/unit/java/android/net/NetworkStatsTest.java
new file mode 100644
index 0000000..23d5a7e
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkStatsTest.java
@@ -0,0 +1,1024 @@
+/*
+ * 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.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.IFACE_ALL;
+import static android.net.NetworkStats.INTERFACES_ALL;
+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 static android.net.NetworkStats.SET_ALL;
+import static android.net.NetworkStats.SET_DBG_VPN_IN;
+import static android.net.NetworkStats.SET_DBG_VPN_OUT;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.SET_FOREGROUND;
+import static android.net.NetworkStats.TAG_ALL;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Process;
+import android.util.ArrayMap;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.android.collect.Sets;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkStatsTest {
+
+    private static final String TEST_IFACE = "test0";
+    private static final String TEST_IFACE2 = "test2";
+    private static final int TEST_UID = 1001;
+    private static final long TEST_START = 1194220800000L;
+
+    @Test
+    public void testFindIndex() throws Exception {
+        final NetworkStats stats = new NetworkStats(TEST_START, 5)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 1024L, 8L, 0L, 0L, 10)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 0L, 0L, 1024L, 8L, 11)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 0L, 0L, 1024L, 8L, 11)
+                .insertEntry(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 1024L, 8L, 1024L, 8L, 12)
+                .insertEntry(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES,
+                        DEFAULT_NETWORK_YES, 1024L, 8L, 1024L, 8L, 12);
+
+        assertEquals(4, stats.findIndex(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_YES,
+                ROAMING_YES, DEFAULT_NETWORK_YES));
+        assertEquals(3, stats.findIndex(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO));
+        assertEquals(2, stats.findIndex(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES,
+                ROAMING_NO, DEFAULT_NETWORK_YES));
+        assertEquals(1, stats.findIndex(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO));
+        assertEquals(0, stats.findIndex(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_YES));
+        assertEquals(-1, stats.findIndex(TEST_IFACE, 6, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO));
+        assertEquals(-1, stats.findIndex(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO));
+    }
+
+    @Test
+    public void testFindIndexHinted() {
+        final NetworkStats stats = new NetworkStats(TEST_START, 3)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 1024L, 8L, 0L, 0L, 10)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 0L, 0L, 1024L, 8L, 11)
+                .insertEntry(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 1024L, 8L, 1024L, 8L, 12)
+                .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 1024L, 8L, 0L, 0L, 10)
+                .insertEntry(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 0L, 0L, 1024L, 8L, 11)
+                .insertEntry(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 0L, 0L, 1024L, 8L, 11)
+                .insertEntry(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 1024L, 8L, 1024L, 8L, 12)
+                .insertEntry(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES,
+                        DEFAULT_NETWORK_NO, 1024L, 8L, 1024L, 8L, 12);
+
+        // verify that we correctly find across regardless of hinting
+        for (int hint = 0; hint < stats.size(); hint++) {
+            assertEquals(0, stats.findIndexHinted(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE,
+                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint));
+            assertEquals(1, stats.findIndexHinted(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE,
+                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, hint));
+            assertEquals(2, stats.findIndexHinted(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE,
+                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint));
+            assertEquals(3, stats.findIndexHinted(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE,
+                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, hint));
+            assertEquals(4, stats.findIndexHinted(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D,
+                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint));
+            assertEquals(5, stats.findIndexHinted(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D,
+                    METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, hint));
+            assertEquals(6, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE,
+                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint));
+            assertEquals(7, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE,
+                    METERED_YES, ROAMING_YES, DEFAULT_NETWORK_NO, hint));
+            assertEquals(-1, stats.findIndexHinted(TEST_IFACE, 6, SET_DEFAULT, TAG_NONE,
+                    METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint));
+            assertEquals(-1, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE,
+                    METERED_YES, ROAMING_YES, DEFAULT_NETWORK_YES, hint));
+        }
+    }
+
+    @Test
+    public void testAddEntryGrow() throws Exception {
+        final NetworkStats stats = new NetworkStats(TEST_START, 4);
+
+        assertEquals(0, stats.size());
+        assertEquals(4, stats.internalSize());
+
+        stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 1L, 1L, 2L, 2L, 3);
+        stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 2L, 2L, 2L, 2L, 4);
+        stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES,
+                DEFAULT_NETWORK_YES, 3L, 3L, 2L, 2L, 5);
+        stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES,
+                DEFAULT_NETWORK_NO, 3L, 3L, 2L, 2L, 5);
+
+        assertEquals(4, stats.size());
+        assertEquals(4, stats.internalSize());
+
+        stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 4L, 40L, 4L, 40L, 7);
+        stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 5L, 50L, 4L, 40L, 8);
+        stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 6L, 60L, 5L, 50L, 10);
+        stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES,
+                DEFAULT_NETWORK_YES, 7L, 70L, 5L, 50L, 11);
+        stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES,
+                DEFAULT_NETWORK_NO, 7L, 70L, 5L, 50L, 11);
+
+        assertEquals(9, stats.size());
+        assertTrue(stats.internalSize() >= 9);
+
+        assertValues(stats, 0, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 1L, 1L, 2L, 2L, 3);
+        assertValues(stats, 1, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 2L, 2L, 2L, 2L, 4);
+        assertValues(stats, 2, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES,
+                DEFAULT_NETWORK_YES, 3L, 3L, 2L, 2L, 5);
+        assertValues(stats, 3, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES,
+                ROAMING_YES, DEFAULT_NETWORK_NO, 3L, 3L, 2L, 2L, 5);
+        assertValues(stats, 4, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 4L, 40L, 4L, 40L, 7);
+        assertValues(stats, 5, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 5L, 50L, 4L, 40L, 8);
+        assertValues(stats, 6, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 6L, 60L, 5L, 50L, 10);
+        assertValues(stats, 7, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES,
+                DEFAULT_NETWORK_YES, 7L, 70L, 5L, 50L, 11);
+        assertValues(stats, 8, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES,
+                ROAMING_YES, DEFAULT_NETWORK_NO, 7L, 70L, 5L, 50L, 11);
+    }
+
+    @Test
+    public void testCombineExisting() throws Exception {
+        final NetworkStats stats = new NetworkStats(TEST_START, 10);
+
+        stats.insertEntry(TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 10);
+        stats.insertEntry(TEST_IFACE, 1001, SET_DEFAULT, 0xff, 128L, 1L, 128L, 1L, 2);
+        stats.combineValues(TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, -128L, -1L,
+                -128L, -1L, -1);
+
+        assertValues(stats, 0, TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 384L, 3L, 128L, 1L, 9);
+        assertValues(stats, 1, TEST_IFACE, 1001, SET_DEFAULT, 0xff, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 128L, 1L, 128L, 1L, 2);
+
+        // now try combining that should create row
+        stats.combineValues(TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 3);
+        assertValues(stats, 2, TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 128L, 1L, 128L, 1L, 3);
+        stats.combineValues(TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 3);
+        assertValues(stats, 2, TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 256L, 2L, 256L, 2L, 6);
+    }
+
+    @Test
+    public void testSubtractIdenticalData() throws Exception {
+        final NetworkStats before = new NetworkStats(TEST_START, 2)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12);
+
+        final NetworkStats after = new NetworkStats(TEST_START, 2)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12);
+
+        final NetworkStats result = after.subtract(before);
+
+        // identical data should result in zero delta
+        assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0);
+        assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0);
+    }
+
+    @Test
+    public void testSubtractIdenticalRows() throws Exception {
+        final NetworkStats before = new NetworkStats(TEST_START, 2)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12);
+
+        final NetworkStats after = new NetworkStats(TEST_START, 2)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1025L, 9L, 2L, 1L, 15)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 3L, 1L, 1028L, 9L, 20);
+
+        final NetworkStats result = after.subtract(before);
+
+        // expect delta between measurements
+        assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 1L, 1L, 2L, 1L, 4);
+        assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 3L, 1L, 4L, 1L, 8);
+    }
+
+    @Test
+    public void testSubtractNewRows() throws Exception {
+        final NetworkStats before = new NetworkStats(TEST_START, 2)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12);
+
+        final NetworkStats after = new NetworkStats(TEST_START, 3)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12)
+                .insertEntry(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 20);
+
+        final NetworkStats result = after.subtract(before);
+
+        // its okay to have new rows
+        assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0);
+        assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0);
+        assertValues(result, 2, TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 1024L, 8L, 1024L, 8L, 20);
+    }
+
+    @Test
+    public void testSubtractMissingRows() throws Exception {
+        final NetworkStats before = new NetworkStats(TEST_START, 2)
+                .insertEntry(TEST_IFACE, UID_ALL, SET_DEFAULT, TAG_NONE, 1024L, 0L, 0L, 0L, 0)
+                .insertEntry(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 2048L, 0L, 0L, 0L, 0);
+
+        final NetworkStats after = new NetworkStats(TEST_START, 1)
+                .insertEntry(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 2049L, 2L, 3L, 4L, 0);
+
+        final NetworkStats result = after.subtract(before);
+
+        // should silently drop omitted rows
+        assertEquals(1, result.size());
+        assertValues(result, 0, TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 2L, 3L, 4L, 0);
+        assertEquals(4L, result.getTotalBytes());
+    }
+
+    @Test
+    public void testTotalBytes() throws Exception {
+        final NetworkStats iface = new NetworkStats(TEST_START, 2)
+                .insertEntry(TEST_IFACE, UID_ALL, SET_DEFAULT, TAG_NONE, 128L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 256L, 0L, 0L, 0L, 0L);
+        assertEquals(384L, iface.getTotalBytes());
+
+        final NetworkStats uidSet = new NetworkStats(TEST_START, 3)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 32L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 32L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_FOREGROUND, TAG_NONE, 32L, 0L, 0L, 0L, 0L);
+        assertEquals(96L, uidSet.getTotalBytes());
+
+        final NetworkStats uidTag = new NetworkStats(TEST_START, 6)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 16L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 16L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 8L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 16L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 16L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 8L, 0L, 0L, 0L, 0L);
+        assertEquals(64L, uidTag.getTotalBytes());
+
+        final NetworkStats uidMetered = new NetworkStats(TEST_START, 3)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L);
+        assertEquals(96L, uidMetered.getTotalBytes());
+
+        final NetworkStats uidRoaming = new NetworkStats(TEST_START, 3)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES,
+                        DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L);
+        assertEquals(96L, uidRoaming.getTotalBytes());
+    }
+
+    @Test
+    public void testGroupedByIfaceEmpty() throws Exception {
+        final NetworkStats uidStats = new NetworkStats(TEST_START, 3);
+        final NetworkStats grouped = uidStats.groupedByIface();
+
+        assertEquals(0, uidStats.size());
+        assertEquals(0, grouped.size());
+    }
+
+    @Test
+    public void testGroupedByIfaceAll() throws Exception {
+        final NetworkStats uidStats = new NetworkStats(TEST_START, 3)
+                .insertEntry(IFACE_ALL, 100, SET_ALL, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 128L, 8L, 0L, 2L, 20L)
+                .insertEntry(IFACE_ALL, 101, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 128L, 8L, 0L, 2L, 20L)
+                .insertEntry(IFACE_ALL, 101, SET_ALL, TAG_NONE, METERED_NO, ROAMING_YES,
+                        DEFAULT_NETWORK_YES, 128L, 8L, 0L, 2L, 20L);
+        final NetworkStats grouped = uidStats.groupedByIface();
+
+        assertEquals(3, uidStats.size());
+        assertEquals(1, grouped.size());
+
+        assertValues(grouped, 0, IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 384L, 24L, 0L, 6L, 0L);
+    }
+
+    @Test
+    public void testGroupedByIface() throws Exception {
+        final NetworkStats uidStats = new NetworkStats(TEST_START, 7)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 128L, 8L, 0L, 2L, 20L)
+                .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 64L, 4L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 128L, 8L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 128L, 8L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES,
+                        DEFAULT_NETWORK_YES, 128L, 8L, 0L, 0L, 0L);
+
+        final NetworkStats grouped = uidStats.groupedByIface();
+
+        assertEquals(7, uidStats.size());
+
+        assertEquals(2, grouped.size());
+        assertValues(grouped, 0, TEST_IFACE, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 384L, 24L, 0L, 2L, 0L);
+        assertValues(grouped, 1, TEST_IFACE2, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 1024L, 64L, 0L, 0L, 0L);
+    }
+
+    @Test
+    public void testAddAllValues() {
+        final NetworkStats first = new NetworkStats(TEST_START, 5)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_YES,
+                        DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L);
+
+        final NetworkStats second = new NetworkStats(TEST_START, 2)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_YES,
+                        DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L);
+
+        first.combineAllValues(second);
+
+        assertEquals(4, first.size());
+        assertValues(first, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 64L, 0L, 0L, 0L, 0L);
+        assertValues(first, 1, TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L);
+        assertValues(first, 2, TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_YES,
+                DEFAULT_NETWORK_YES, 64L, 0L, 0L, 0L, 0L);
+        assertValues(first, 3, TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L);
+    }
+
+    @Test
+    public void testGetTotal() {
+        final NetworkStats stats = new NetworkStats(TEST_START, 7)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 128L, 8L, 0L, 2L, 20L)
+                .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 64L, 4L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 512L,32L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 128L, 8L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 128L, 8L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES,
+                        DEFAULT_NETWORK_NO, 128L, 8L, 0L, 0L, 0L);
+
+        assertValues(stats.getTotal(null), 1408L, 88L, 0L, 2L, 20L);
+        assertValues(stats.getTotal(null, 100), 1280L, 80L, 0L, 2L, 20L);
+        assertValues(stats.getTotal(null, 101), 128L, 8L, 0L, 0L, 0L);
+
+        final HashSet<String> ifaces = Sets.newHashSet();
+        assertValues(stats.getTotal(null, ifaces), 0L, 0L, 0L, 0L, 0L);
+
+        ifaces.add(TEST_IFACE2);
+        assertValues(stats.getTotal(null, ifaces), 1024L, 64L, 0L, 0L, 0L);
+    }
+
+    @Test
+    public void testRemoveUids() throws Exception {
+        final NetworkStats before = new NetworkStats(TEST_START, 3);
+
+        // Test 0 item stats.
+        NetworkStats after = before.clone();
+        after.removeUids(new int[0]);
+        assertEquals(0, after.size());
+        after.removeUids(new int[] {100});
+        assertEquals(0, after.size());
+
+        // Test 1 item stats.
+        before.insertEntry(TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, 1L, 128L, 0L, 2L, 20L);
+        after = before.clone();
+        after.removeUids(new int[0]);
+        assertEquals(1, after.size());
+        assertValues(after, 0, TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 1L, 128L, 0L, 2L, 20L);
+        after.removeUids(new int[] {99});
+        assertEquals(0, after.size());
+
+        // Append remaining test items.
+        before.insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 2L, 64L, 0L, 2L, 20L)
+                .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 4L, 32L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 8L, 16L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 16L, 8L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 32L, 4L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 64L, 2L, 0L, 0L, 0L);
+        assertEquals(7, before.size());
+
+        // Test remove with empty uid list.
+        after = before.clone();
+        after.removeUids(new int[0]);
+        assertValues(after.getTotalIncludingTags(null), 127L, 254L, 0L, 4L, 40L);
+
+        // Test remove uids don't exist in stats.
+        after.removeUids(new int[] {98, 0, Integer.MIN_VALUE, Integer.MAX_VALUE});
+        assertValues(after.getTotalIncludingTags(null), 127L, 254L, 0L, 4L, 40L);
+
+        // Test remove all uids.
+        after.removeUids(new int[] {99, 100, 100, 101});
+        assertEquals(0, after.size());
+
+        // Test remove in the middle.
+        after = before.clone();
+        after.removeUids(new int[] {100});
+        assertEquals(3, after.size());
+        assertValues(after, 0, TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 1L, 128L, 0L, 2L, 20L);
+        assertValues(after, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 32L, 4L, 0L, 0L, 0L);
+        assertValues(after, 2, TEST_IFACE, 101, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 64L, 2L, 0L, 0L, 0L);
+    }
+
+    @Test
+    public void testRemoveEmptyEntries() throws Exception {
+        // Test empty stats.
+        final NetworkStats statsEmpty = new NetworkStats(TEST_START, 3);
+        assertEquals(0, statsEmpty.removeEmptyEntries().size());
+
+        // Test stats with non-zero entry.
+        final NetworkStats statsNonZero = new NetworkStats(TEST_START, 1)
+                .insertEntry(TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO,
+                        ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 128L, 0L, 2L, 20L);
+        assertEquals(1, statsNonZero.size());
+        final NetworkStats expectedNonZero = statsNonZero.removeEmptyEntries();
+        assertEquals(1, expectedNonZero.size());
+        assertValues(expectedNonZero, 0, TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 128L, 0L, 2L, 20L);
+
+        // Test stats with empty entry.
+        final NetworkStats statsZero = new NetworkStats(TEST_START, 1)
+                .insertEntry(TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO,
+                        ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L);
+        assertEquals(1, statsZero.size());
+        final NetworkStats expectedZero = statsZero.removeEmptyEntries();
+        assertEquals(1, statsZero.size()); // Assert immutable.
+        assertEquals(0, expectedZero.size());
+
+        // Test stats with multiple entries.
+        final NetworkStats statsMultiple = new NetworkStats(TEST_START, 0)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 2L, 64L, 0L, 2L, 20L)
+                .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 4L, 32L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 8L, 0L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 0L, 8L, 0L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 4L, 0L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 0L, 2L, 0L)
+                .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 0L, 0L, 1L);
+        assertEquals(9, statsMultiple.size());
+        final NetworkStats expectedMultiple = statsMultiple.removeEmptyEntries();
+        assertEquals(9, statsMultiple.size()); // Assert immutable.
+        assertEquals(7, expectedMultiple.size());
+        assertValues(expectedMultiple.getTotalIncludingTags(null), 14L, 104L, 4L, 4L, 21L);
+
+        // Test stats with multiple empty entries.
+        assertEquals(statsMultiple.size(), statsMultiple.subtract(statsMultiple).size());
+        assertEquals(0, statsMultiple.subtract(statsMultiple).removeEmptyEntries().size());
+    }
+
+    @Test
+    public void testClone() throws Exception {
+        final NetworkStats original = new NetworkStats(TEST_START, 5)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 2L, 20L)
+                .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 512L, 32L, 0L, 0L, 0L);
+
+        // make clone and mutate original
+        final NetworkStats clone = original.clone();
+        original.insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 0L, 0L);
+
+        assertEquals(3, original.size());
+        assertEquals(2, clone.size());
+
+        assertEquals(128L + 512L + 128L, original.getTotalBytes());
+        assertEquals(128L + 512L, clone.getTotalBytes());
+    }
+
+    @Test
+    public void testAddWhenEmpty() throws Exception {
+        final NetworkStats red = new NetworkStats(TEST_START, -1);
+        final NetworkStats blue = new NetworkStats(TEST_START, 5)
+                .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 2L, 20L)
+                .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 512L, 32L, 0L, 0L, 0L);
+
+        // We're mostly checking that we don't crash
+        red.combineAllValues(blue);
+    }
+
+    @Test
+    public void testMigrateTun() throws Exception {
+        final int tunUid = 10030;
+        final String tunIface = "tun0";
+        final String underlyingIface = "wlan0";
+        final int testTag1 = 8888;
+        NetworkStats delta = new NetworkStats(TEST_START, 17)
+                .insertEntry(tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 39605L, 46L, 12259L, 55L, 0L)
+                .insertEntry(tunIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L)
+                .insertEntry(tunIface, 10120, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 72667L, 197L, 43909L, 241L, 0L)
+                .insertEntry(tunIface, 10120, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 9297L, 17L, 4128L, 21L, 0L)
+                // VPN package also uses some traffic through unprotected network.
+                .insertEntry(tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 4983L, 10L, 1801L, 12L, 0L)
+                .insertEntry(tunIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L)
+                // Tag entries
+                .insertEntry(tunIface, 10120, SET_DEFAULT, testTag1, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 21691L, 41L, 13820L, 51L, 0L)
+                .insertEntry(tunIface, 10120, SET_FOREGROUND, testTag1, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 1281L, 2L, 665L, 2L, 0L)
+                // Irrelevant entries
+                .insertEntry(TEST_IFACE, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 1685L, 5L, 2070L, 6L, 0L)
+                // Underlying Iface entries
+                .insertEntry(underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 5178L, 8L, 2139L, 11L, 0L)
+                .insertEntry(underlyingIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO,
+                        ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L)
+                .insertEntry(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 149873L, 287L, 59217L /* smaller than sum(tun0) */,
+                        299L /* smaller than sum(tun0) */, 0L)
+                .insertEntry(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO,
+                        ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L);
+
+        delta.migrateTun(tunUid, tunIface, Arrays.asList(underlyingIface));
+        assertEquals(20, delta.size());
+
+        // tunIface and TEST_IFACE entries are not changed.
+        assertValues(delta, 0, tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 39605L, 46L, 12259L, 55L, 0L);
+        assertValues(delta, 1, tunIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L);
+        assertValues(delta, 2, tunIface, 10120, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 72667L, 197L, 43909L, 241L, 0L);
+        assertValues(delta, 3, tunIface, 10120, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 9297L, 17L, 4128L, 21L, 0L);
+        assertValues(delta, 4, tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 4983L, 10L, 1801L, 12L, 0L);
+        assertValues(delta, 5, tunIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L);
+        assertValues(delta, 6, tunIface, 10120, SET_DEFAULT, testTag1, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 21691L, 41L, 13820L, 51L, 0L);
+        assertValues(delta, 7, tunIface, 10120, SET_FOREGROUND, testTag1, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 1281L, 2L, 665L, 2L, 0L);
+        assertValues(delta, 8, TEST_IFACE, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 1685L, 5L, 2070L, 6L, 0L);
+
+        // Existing underlying Iface entries are updated
+        assertValues(delta, 9, underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 44783L, 54L, 14178L, 62L, 0L);
+        assertValues(delta, 10, underlyingIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L);
+
+        // VPN underlying Iface entries are updated
+        assertValues(delta, 11, underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 28304L, 27L, 1L, 2L, 0L);
+        assertValues(delta, 12, underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L);
+
+        // New entries are added for new application's underlying Iface traffic
+        assertContains(delta, underlyingIface, 10120, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 72667L, 197L, 43123L, 227L, 0L);
+        assertContains(delta, underlyingIface, 10120, SET_FOREGROUND, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 9297L, 17L, 4054, 19L, 0L);
+        assertContains(delta, underlyingIface, 10120, SET_DEFAULT, testTag1, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 21691L, 41L, 13572L, 48L, 0L);
+        assertContains(delta, underlyingIface, 10120, SET_FOREGROUND, testTag1, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 1281L, 2L, 653L, 1L, 0L);
+
+        // New entries are added for debug purpose
+        assertContains(delta, underlyingIface, 10100, SET_DBG_VPN_IN, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 39605L, 46L, 12039, 51, 0);
+        assertContains(delta, underlyingIface, 10120, SET_DBG_VPN_IN, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 81964, 214, 47177, 246, 0);
+        assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_OUT, TAG_NONE, METERED_ALL,
+                ROAMING_ALL, DEFAULT_NETWORK_ALL, 121569, 260, 59216, 297, 0);
+
+    }
+
+    // Tests a case where all of the data received by the tun0 interface is echo back into the tun0
+    // interface by the vpn app before it's sent out of the underlying interface. The VPN app should
+    // not be charged for the echoed data but it should still be charged for any extra data it sends
+    // via the underlying interface.
+    @Test
+    public void testMigrateTun_VpnAsLoopback() {
+        final int tunUid = 10030;
+        final String tunIface = "tun0";
+        final String underlyingIface = "wlan0";
+        NetworkStats delta = new NetworkStats(TEST_START, 9)
+                // 2 different apps sent/receive data via tun0.
+                .insertEntry(tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L)
+                .insertEntry(tunIface, 20100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 500L, 2L, 200L, 5L, 0L)
+                // VPN package resends data through the tunnel (with exaggerated overhead)
+                .insertEntry(tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 240000, 100L, 120000L, 60L, 0L)
+                // 1 app already has some traffic on the underlying interface, the other doesn't yet
+                .insertEntry(underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 1000L, 10L, 2000L, 20L, 0L)
+                // Traffic through the underlying interface via the vpn app.
+                // This test should redistribute this data correctly.
+                .insertEntry(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 75500L, 37L, 130000L, 70L, 0L);
+
+        delta.migrateTun(tunUid, tunIface, Arrays.asList(underlyingIface));
+        assertEquals(9, delta.size());
+
+        // tunIface entries should not be changed.
+        assertValues(delta, 0, tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+        assertValues(delta, 1, tunIface, 20100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 500L, 2L, 200L, 5L, 0L);
+        assertValues(delta, 2, tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 240000L, 100L, 120000L, 60L, 0L);
+
+        // Existing underlying Iface entries are updated
+        assertValues(delta, 3, underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 51000L, 35L, 102000L, 70L, 0L);
+
+        // VPN underlying Iface entries are updated
+        assertValues(delta, 4, underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 25000L, 10L, 29800L, 15L, 0L);
+
+        // New entries are added for new application's underlying Iface traffic
+        assertContains(delta, underlyingIface, 20100, SET_DEFAULT, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 500L, 2L, 200L, 5L, 0L);
+
+        // New entries are added for debug purpose
+        assertContains(delta, underlyingIface, 10100, SET_DBG_VPN_IN, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+        assertContains(delta, underlyingIface, 20100, SET_DBG_VPN_IN, TAG_NONE, METERED_NO,
+                ROAMING_NO, DEFAULT_NETWORK_NO, 500, 2L, 200L, 5L, 0L);
+        assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_OUT, TAG_NONE, METERED_ALL,
+                ROAMING_ALL, DEFAULT_NETWORK_ALL, 50500L, 27L, 100200L, 55, 0);
+    }
+
+    @Test
+    public void testFilter_NoFilter() {
+        NetworkStats.Entry entry1 = new NetworkStats.Entry(
+                "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry2 = new NetworkStats.Entry(
+                "test2", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry3 = new NetworkStats.Entry(
+                "test2", 10101, SET_DEFAULT, 123, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats stats = new NetworkStats(TEST_START, 3)
+                .insertEntry(entry1)
+                .insertEntry(entry2)
+                .insertEntry(entry3);
+
+        stats.filter(UID_ALL, INTERFACES_ALL, TAG_ALL);
+        assertEquals(3, stats.size());
+        assertEquals(entry1, stats.getValues(0, null));
+        assertEquals(entry2, stats.getValues(1, null));
+        assertEquals(entry3, stats.getValues(2, null));
+    }
+
+    @Test
+    public void testFilter_UidFilter() {
+        final int testUid = 10101;
+        NetworkStats.Entry entry1 = new NetworkStats.Entry(
+                "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry2 = new NetworkStats.Entry(
+                "test2", testUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry3 = new NetworkStats.Entry(
+                "test2", testUid, SET_DEFAULT, 123, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats stats = new NetworkStats(TEST_START, 3)
+                .insertEntry(entry1)
+                .insertEntry(entry2)
+                .insertEntry(entry3);
+
+        stats.filter(testUid, INTERFACES_ALL, TAG_ALL);
+        assertEquals(2, stats.size());
+        assertEquals(entry2, stats.getValues(0, null));
+        assertEquals(entry3, stats.getValues(1, null));
+    }
+
+    @Test
+    public void testFilter_InterfaceFilter() {
+        final String testIf1 = "testif1";
+        final String testIf2 = "testif2";
+        NetworkStats.Entry entry1 = new NetworkStats.Entry(
+                testIf1, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry2 = new NetworkStats.Entry(
+                "otherif", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry3 = new NetworkStats.Entry(
+                testIf1, 10101, SET_DEFAULT, 123, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry4 = new NetworkStats.Entry(
+                testIf2, 10101, SET_DEFAULT, 123, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats stats = new NetworkStats(TEST_START, 4)
+                .insertEntry(entry1)
+                .insertEntry(entry2)
+                .insertEntry(entry3)
+                .insertEntry(entry4);
+
+        stats.filter(UID_ALL, new String[] { testIf1, testIf2 }, TAG_ALL);
+        assertEquals(3, stats.size());
+        assertEquals(entry1, stats.getValues(0, null));
+        assertEquals(entry3, stats.getValues(1, null));
+        assertEquals(entry4, stats.getValues(2, null));
+    }
+
+    @Test
+    public void testFilter_EmptyInterfaceFilter() {
+        NetworkStats.Entry entry1 = new NetworkStats.Entry(
+                "if1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry2 = new NetworkStats.Entry(
+                "if2", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats stats = new NetworkStats(TEST_START, 3)
+                .insertEntry(entry1)
+                .insertEntry(entry2);
+
+        stats.filter(UID_ALL, new String[] { }, TAG_ALL);
+        assertEquals(0, stats.size());
+    }
+
+    @Test
+    public void testFilter_TagFilter() {
+        final int testTag = 123;
+        final int otherTag = 456;
+        NetworkStats.Entry entry1 = new NetworkStats.Entry(
+                "test1", 10100, SET_DEFAULT, testTag, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry2 = new NetworkStats.Entry(
+                "test2", 10101, SET_DEFAULT, testTag, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry3 = new NetworkStats.Entry(
+                "test2", 10101, SET_DEFAULT, otherTag, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats stats = new NetworkStats(TEST_START, 3)
+                .insertEntry(entry1)
+                .insertEntry(entry2)
+                .insertEntry(entry3);
+
+        stats.filter(UID_ALL, INTERFACES_ALL, testTag);
+        assertEquals(2, stats.size());
+        assertEquals(entry1, stats.getValues(0, null));
+        assertEquals(entry2, stats.getValues(1, null));
+    }
+
+    @Test
+    public void testFilterDebugEntries() {
+        NetworkStats.Entry entry1 = new NetworkStats.Entry(
+                "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry2 = new NetworkStats.Entry(
+                "test2", 10101, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry3 = new NetworkStats.Entry(
+                "test2", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry4 = new NetworkStats.Entry(
+                "test2", 10101, SET_DBG_VPN_OUT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats stats = new NetworkStats(TEST_START, 4)
+                .insertEntry(entry1)
+                .insertEntry(entry2)
+                .insertEntry(entry3)
+                .insertEntry(entry4);
+
+        stats.filterDebugEntries();
+
+        assertEquals(2, stats.size());
+        assertEquals(entry1, stats.getValues(0, null));
+        assertEquals(entry3, stats.getValues(1, null));
+    }
+
+    @Test
+    public void testApply464xlatAdjustments() {
+        final String v4Iface = "v4-wlan0";
+        final String baseIface = "wlan0";
+        final String otherIface = "other";
+        final int appUid = 10001;
+        final int rootUid = Process.ROOT_UID;
+        ArrayMap<String, String> stackedIface = new ArrayMap<>();
+        stackedIface.put(v4Iface, baseIface);
+
+        // Ipv4 traffic sent/received by an app on stacked interface.
+        final NetworkStats.Entry appEntry = new NetworkStats.Entry(
+                v4Iface, appUid, SET_DEFAULT, TAG_NONE,
+                30501490  /* rxBytes */,
+                22401 /* rxPackets */,
+                876235 /* txBytes */,
+                13805 /* txPackets */,
+                0 /* operations */);
+
+        // Traffic measured for the root uid on the base interface.
+        final NetworkStats.Entry rootUidEntry = new NetworkStats.Entry(
+                baseIface, rootUid, SET_DEFAULT, TAG_NONE,
+                163577 /* rxBytes */,
+                187 /* rxPackets */,
+                17607 /* txBytes */,
+                97 /* txPackets */,
+                0 /* operations */);
+
+        final NetworkStats.Entry otherEntry = new NetworkStats.Entry(
+                otherIface, appUid, SET_DEFAULT, TAG_NONE,
+                2600  /* rxBytes */,
+                2 /* rxPackets */,
+                3800 /* txBytes */,
+                3 /* txPackets */,
+                0 /* operations */);
+
+        final NetworkStats stats = new NetworkStats(TEST_START, 3)
+                .insertEntry(appEntry)
+                .insertEntry(rootUidEntry)
+                .insertEntry(otherEntry);
+
+        stats.apply464xlatAdjustments(stackedIface);
+
+        assertEquals(3, stats.size());
+        final NetworkStats.Entry expectedAppUid = new NetworkStats.Entry(
+                v4Iface, appUid, SET_DEFAULT, TAG_NONE,
+                30949510,
+                22401,
+                1152335,
+                13805,
+                0);
+        final NetworkStats.Entry expectedRootUid = new NetworkStats.Entry(
+                baseIface, 0, SET_DEFAULT, TAG_NONE,
+                163577,
+                187,
+                17607,
+                97,
+                0);
+        assertEquals(expectedAppUid, stats.getValues(0, null));
+        assertEquals(expectedRootUid, stats.getValues(1, null));
+        assertEquals(otherEntry, stats.getValues(2, null));
+    }
+
+    @Test
+    public void testApply464xlatAdjustments_noStackedIface() {
+        NetworkStats.Entry firstEntry = new NetworkStats.Entry(
+                "if1", 10002, SET_DEFAULT, TAG_NONE,
+                2600  /* rxBytes */,
+                2 /* rxPackets */,
+                3800 /* txBytes */,
+                3 /* txPackets */,
+                0 /* operations */);
+        NetworkStats.Entry secondEntry = new NetworkStats.Entry(
+                "if2", 10002, SET_DEFAULT, TAG_NONE,
+                5000  /* rxBytes */,
+                3 /* rxPackets */,
+                6000 /* txBytes */,
+                4 /* txPackets */,
+                0 /* operations */);
+
+        NetworkStats stats = new NetworkStats(TEST_START, 2)
+                .insertEntry(firstEntry)
+                .insertEntry(secondEntry);
+
+        // Empty map: no adjustment
+        stats.apply464xlatAdjustments(new ArrayMap<>());
+
+        assertEquals(2, stats.size());
+        assertEquals(firstEntry, stats.getValues(0, null));
+        assertEquals(secondEntry, stats.getValues(1, null));
+    }
+
+    private static void assertContains(NetworkStats stats,  String iface, int uid, int set,
+            int tag, int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets,
+            long txBytes, long txPackets, long operations) {
+        int index = stats.findIndex(iface, uid, set, tag, metered, roaming, defaultNetwork);
+        assertTrue(index != -1);
+        assertValues(stats, index, iface, uid, set, tag, metered, roaming, defaultNetwork,
+                rxBytes, rxPackets, txBytes, txPackets, operations);
+    }
+
+    private static void assertValues(NetworkStats stats, int index, String iface, int uid, int set,
+            int tag, int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets,
+            long txBytes, long txPackets, long operations) {
+        final NetworkStats.Entry entry = stats.getValues(index, null);
+        assertValues(entry, iface, uid, set, tag, metered, roaming, defaultNetwork);
+        assertValues(entry, rxBytes, rxPackets, txBytes, txPackets, operations);
+    }
+
+    private static void assertValues(
+            NetworkStats.Entry entry, String iface, int uid, int set, int tag, int metered,
+            int roaming, int defaultNetwork) {
+        assertEquals(iface, entry.iface);
+        assertEquals(uid, entry.uid);
+        assertEquals(set, entry.set);
+        assertEquals(tag, entry.tag);
+        assertEquals(metered, entry.metered);
+        assertEquals(roaming, entry.roaming);
+        assertEquals(defaultNetwork, entry.defaultNetwork);
+    }
+
+    private static void assertValues(NetworkStats.Entry entry, long rxBytes, long rxPackets,
+            long txBytes, long txPackets, long operations) {
+        assertEquals(rxBytes, entry.rxBytes);
+        assertEquals(rxPackets, entry.rxPackets);
+        assertEquals(txBytes, entry.txBytes);
+        assertEquals(txPackets, entry.txPackets);
+        assertEquals(operations, entry.operations);
+    }
+
+}
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
new file mode 100644
index 0000000..cb39a0c
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -0,0 +1,365 @@
+/*
+ * 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
+
+import android.content.Context
+import android.net.ConnectivityManager.TYPE_MOBILE
+import android.net.ConnectivityManager.TYPE_WIFI
+import android.net.NetworkIdentity.SUBTYPE_COMBINED
+import android.net.NetworkIdentity.OEM_NONE
+import android.net.NetworkIdentity.OEM_PAID
+import android.net.NetworkIdentity.OEM_PRIVATE
+import android.net.NetworkIdentity.buildNetworkIdentity
+import android.net.NetworkStats.DEFAULT_NETWORK_ALL
+import android.net.NetworkStats.METERED_ALL
+import android.net.NetworkStats.ROAMING_ALL
+import android.net.NetworkTemplate.MATCH_MOBILE
+import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
+import android.net.NetworkTemplate.MATCH_WIFI
+import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
+import android.net.NetworkTemplate.WIFI_NETWORKID_ALL
+import android.net.NetworkTemplate.NETWORK_TYPE_5G_NSA
+import android.net.NetworkTemplate.NETWORK_TYPE_ALL
+import android.net.NetworkTemplate.OEM_MANAGED_ALL
+import android.net.NetworkTemplate.OEM_MANAGED_NO
+import android.net.NetworkTemplate.OEM_MANAGED_YES
+import android.net.NetworkTemplate.SUBSCRIBER_ID_MATCH_RULE_EXACT
+import android.net.NetworkTemplate.buildTemplateWifi
+import android.net.NetworkTemplate.buildTemplateWifiWildcard
+import android.net.NetworkTemplate.buildTemplateCarrierMetered
+import android.net.NetworkTemplate.buildTemplateMobileWithRatType
+import android.telephony.TelephonyManager
+import com.android.testutils.assertParcelSane
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+
+private const val TEST_IMSI1 = "imsi1"
+private const val TEST_IMSI2 = "imsi2"
+private const val TEST_SSID1 = "ssid1"
+private const val TEST_SSID2 = "ssid2"
+
+@RunWith(JUnit4::class)
+class NetworkTemplateTest {
+    private val mockContext = mock(Context::class.java)
+
+    private fun buildMobileNetworkState(subscriberId: String): NetworkStateSnapshot =
+            buildNetworkState(TYPE_MOBILE, subscriberId = subscriberId)
+    private fun buildWifiNetworkState(subscriberId: String?, ssid: String?): NetworkStateSnapshot =
+            buildNetworkState(TYPE_WIFI, subscriberId = subscriberId, ssid = ssid)
+
+    private fun buildNetworkState(
+        type: Int,
+        subscriberId: String? = null,
+        ssid: String? = null,
+        oemManaged: Int = OEM_NONE,
+        metered: Boolean = true
+    ): NetworkStateSnapshot {
+        val lp = LinkProperties()
+        val caps = NetworkCapabilities().apply {
+            setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, !metered)
+            setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true)
+            setSSID(ssid)
+            setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID,
+                    (oemManaged and OEM_PAID) == OEM_PAID)
+            setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE,
+                    (oemManaged and OEM_PRIVATE) == OEM_PRIVATE)
+        }
+        return NetworkStateSnapshot(mock(Network::class.java), caps, lp, subscriberId, type)
+    }
+
+    private fun NetworkTemplate.assertMatches(ident: NetworkIdentity) =
+            assertTrue(matches(ident), "$this does not match $ident")
+
+    private fun NetworkTemplate.assertDoesNotMatch(ident: NetworkIdentity) =
+            assertFalse(matches(ident), "$this should match $ident")
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun testWifiWildcardMatches() {
+        val templateWifiWildcard = buildTemplateWifiWildcard()
+
+        val identMobileImsi1 = buildNetworkIdentity(mockContext,
+                buildMobileNetworkState(TEST_IMSI1),
+                false, TelephonyManager.NETWORK_TYPE_UMTS)
+        val identWifiImsiNullSsid1 = buildNetworkIdentity(
+                mockContext, buildWifiNetworkState(null, TEST_SSID1), true, 0)
+        val identWifiImsi1Ssid1 = buildNetworkIdentity(
+                mockContext, buildWifiNetworkState(TEST_IMSI1, TEST_SSID1), true, 0)
+
+        templateWifiWildcard.assertDoesNotMatch(identMobileImsi1)
+        templateWifiWildcard.assertMatches(identWifiImsiNullSsid1)
+        templateWifiWildcard.assertMatches(identWifiImsi1Ssid1)
+    }
+
+    @Test
+    fun testWifiMatches() {
+        val templateWifiSsid1 = buildTemplateWifi(TEST_SSID1)
+        val templateWifiSsid1ImsiNull = buildTemplateWifi(TEST_SSID1, null)
+        val templateWifiSsid1Imsi1 = buildTemplateWifi(TEST_SSID1, TEST_IMSI1)
+        val templateWifiSsidAllImsi1 = buildTemplateWifi(WIFI_NETWORKID_ALL, TEST_IMSI1)
+
+        val identMobile1 = buildNetworkIdentity(mockContext, buildMobileNetworkState(TEST_IMSI1),
+                false, TelephonyManager.NETWORK_TYPE_UMTS)
+        val identWifiImsiNullSsid1 = buildNetworkIdentity(
+                mockContext, buildWifiNetworkState(null, TEST_SSID1), true, 0)
+        val identWifiImsi1Ssid1 = buildNetworkIdentity(
+                mockContext, buildWifiNetworkState(TEST_IMSI1, TEST_SSID1), true, 0)
+        val identWifiImsi2Ssid1 = buildNetworkIdentity(
+                mockContext, buildWifiNetworkState(TEST_IMSI2, TEST_SSID1), true, 0)
+        val identWifiImsi1Ssid2 = buildNetworkIdentity(
+                mockContext, buildWifiNetworkState(TEST_IMSI1, TEST_SSID2), true, 0)
+
+        // Verify that template with SSID only matches any subscriberId and specific SSID.
+        templateWifiSsid1.assertDoesNotMatch(identMobile1)
+        templateWifiSsid1.assertMatches(identWifiImsiNullSsid1)
+        templateWifiSsid1.assertMatches(identWifiImsi1Ssid1)
+        templateWifiSsid1.assertMatches(identWifiImsi2Ssid1)
+        templateWifiSsid1.assertDoesNotMatch(identWifiImsi1Ssid2)
+
+        // Verify that template with SSID1 and null imsi matches any network with
+        // SSID1 and null imsi.
+        templateWifiSsid1ImsiNull.assertDoesNotMatch(identMobile1)
+        templateWifiSsid1ImsiNull.assertMatches(identWifiImsiNullSsid1)
+        templateWifiSsid1ImsiNull.assertDoesNotMatch(identWifiImsi1Ssid1)
+        templateWifiSsid1ImsiNull.assertDoesNotMatch(identWifiImsi2Ssid1)
+        templateWifiSsid1ImsiNull.assertDoesNotMatch(identWifiImsi1Ssid2)
+
+        // Verify that template with SSID1 and imsi1 matches any network with
+        // SSID1 and imsi1.
+        templateWifiSsid1Imsi1.assertDoesNotMatch(identMobile1)
+        templateWifiSsid1Imsi1.assertDoesNotMatch(identWifiImsiNullSsid1)
+        templateWifiSsid1Imsi1.assertMatches(identWifiImsi1Ssid1)
+        templateWifiSsid1Imsi1.assertDoesNotMatch(identWifiImsi2Ssid1)
+        templateWifiSsid1Imsi1.assertDoesNotMatch(identWifiImsi1Ssid2)
+
+        // Verify that template with SSID all and imsi1 matches any network with
+        // any SSID and imsi1.
+        templateWifiSsidAllImsi1.assertDoesNotMatch(identMobile1)
+        templateWifiSsidAllImsi1.assertDoesNotMatch(identWifiImsiNullSsid1)
+        templateWifiSsidAllImsi1.assertMatches(identWifiImsi1Ssid1)
+        templateWifiSsidAllImsi1.assertDoesNotMatch(identWifiImsi2Ssid1)
+        templateWifiSsidAllImsi1.assertMatches(identWifiImsi1Ssid2)
+    }
+
+    @Test
+    fun testCarrierMeteredMatches() {
+        val templateCarrierImsi1Metered = buildTemplateCarrierMetered(TEST_IMSI1)
+
+        val mobileImsi1 = buildMobileNetworkState(TEST_IMSI1)
+        val mobileImsi1Unmetered = buildNetworkState(TYPE_MOBILE, TEST_IMSI1, null /* ssid */,
+                OEM_NONE, false /* metered */)
+        val mobileImsi2 = buildMobileNetworkState(TEST_IMSI2)
+        val wifiSsid1 = buildWifiNetworkState(null /* subscriberId */, TEST_SSID1)
+        val wifiImsi1Ssid1 = buildWifiNetworkState(TEST_IMSI1, TEST_SSID1)
+        val wifiImsi1Ssid1Unmetered = buildNetworkState(TYPE_WIFI, TEST_IMSI1, TEST_SSID1,
+                OEM_NONE, false /* metered */)
+
+        val identMobileImsi1Metered = buildNetworkIdentity(mockContext,
+                mobileImsi1, false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+        val identMobileImsi1Unmetered = buildNetworkIdentity(mockContext,
+                mobileImsi1Unmetered, false /* defaultNetwork */,
+                TelephonyManager.NETWORK_TYPE_UMTS)
+        val identMobileImsi2Metered = buildNetworkIdentity(mockContext,
+                mobileImsi2, false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+        val identWifiSsid1Metered = buildNetworkIdentity(
+                mockContext, wifiSsid1, true /* defaultNetwork */, 0 /* subType */)
+        val identCarrierWifiImsi1Metered = buildNetworkIdentity(
+                mockContext, wifiImsi1Ssid1, true /* defaultNetwork */, 0 /* subType */)
+        val identCarrierWifiImsi1NonMetered = buildNetworkIdentity(mockContext,
+                wifiImsi1Ssid1Unmetered, true /* defaultNetwork */, 0 /* subType */)
+
+        templateCarrierImsi1Metered.assertMatches(identMobileImsi1Metered)
+        templateCarrierImsi1Metered.assertDoesNotMatch(identMobileImsi1Unmetered)
+        templateCarrierImsi1Metered.assertDoesNotMatch(identMobileImsi2Metered)
+        templateCarrierImsi1Metered.assertDoesNotMatch(identWifiSsid1Metered)
+        templateCarrierImsi1Metered.assertMatches(identCarrierWifiImsi1Metered)
+        templateCarrierImsi1Metered.assertDoesNotMatch(identCarrierWifiImsi1NonMetered)
+    }
+
+    @Test
+    fun testRatTypeGroupMatches() {
+        val stateMobile = buildMobileNetworkState(TEST_IMSI1)
+        // Build UMTS template that matches mobile identities with RAT in the same
+        // group with any IMSI. See {@link NetworkTemplate#getCollapsedRatType}.
+        val templateUmts = buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS)
+        // Build normal template that matches mobile identities with any RAT and IMSI.
+        val templateAll = buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL)
+        // Build template with UNKNOWN RAT that matches mobile identities with RAT that
+        // cannot be determined.
+        val templateUnknown =
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN)
+
+        val identUmts = buildNetworkIdentity(
+                mockContext, stateMobile, false, TelephonyManager.NETWORK_TYPE_UMTS)
+        val identHsdpa = buildNetworkIdentity(
+                mockContext, stateMobile, false, TelephonyManager.NETWORK_TYPE_HSDPA)
+        val identLte = buildNetworkIdentity(
+                mockContext, stateMobile, false, TelephonyManager.NETWORK_TYPE_LTE)
+        val identCombined = buildNetworkIdentity(
+                mockContext, stateMobile, false, SUBTYPE_COMBINED)
+        val identImsi2 = buildNetworkIdentity(mockContext, buildMobileNetworkState(TEST_IMSI2),
+                false, TelephonyManager.NETWORK_TYPE_UMTS)
+        val identWifi = buildNetworkIdentity(
+                mockContext, buildWifiNetworkState(null, TEST_SSID1), true, 0)
+
+        // Assert that identity with the same RAT matches.
+        templateUmts.assertMatches(identUmts)
+        templateAll.assertMatches(identUmts)
+        templateUnknown.assertDoesNotMatch(identUmts)
+        // Assert that identity with the RAT within the same group matches.
+        templateUmts.assertMatches(identHsdpa)
+        templateAll.assertMatches(identHsdpa)
+        templateUnknown.assertDoesNotMatch(identHsdpa)
+        // Assert that identity with the RAT out of the same group only matches template with
+        // NETWORK_TYPE_ALL.
+        templateUmts.assertDoesNotMatch(identLte)
+        templateAll.assertMatches(identLte)
+        templateUnknown.assertDoesNotMatch(identLte)
+        // Assert that identity with combined RAT only matches with template with NETWORK_TYPE_ALL
+        // and NETWORK_TYPE_UNKNOWN.
+        templateUmts.assertDoesNotMatch(identCombined)
+        templateAll.assertMatches(identCombined)
+        templateUnknown.assertMatches(identCombined)
+        // Assert that identity with different IMSI matches.
+        templateUmts.assertMatches(identImsi2)
+        templateAll.assertMatches(identImsi2)
+        templateUnknown.assertDoesNotMatch(identImsi2)
+        // Assert that wifi identity does not match.
+        templateUmts.assertDoesNotMatch(identWifi)
+        templateAll.assertDoesNotMatch(identWifi)
+        templateUnknown.assertDoesNotMatch(identWifi)
+    }
+
+    @Test
+    fun testParcelUnparcel() {
+        val templateMobile = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1, null, null, METERED_ALL,
+                ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_LTE,
+                OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+        val templateWifi = NetworkTemplate(MATCH_WIFI, null, null, TEST_SSID1, METERED_ALL,
+                ROAMING_ALL, DEFAULT_NETWORK_ALL, 0, OEM_MANAGED_ALL,
+                SUBSCRIBER_ID_MATCH_RULE_EXACT)
+        val templateOem = NetworkTemplate(MATCH_MOBILE, null, null, null, METERED_ALL,
+                ROAMING_ALL, DEFAULT_NETWORK_ALL, 0, OEM_MANAGED_YES,
+                SUBSCRIBER_ID_MATCH_RULE_EXACT)
+        assertParcelSane(templateMobile, 10)
+        assertParcelSane(templateWifi, 10)
+        assertParcelSane(templateOem, 10)
+    }
+
+    // Verify NETWORK_TYPE_* constants in NetworkTemplate do not conflict with
+    // TelephonyManager#NETWORK_TYPE_* constants.
+    @Test
+    fun testNetworkTypeConstants() {
+        for (ratType in TelephonyManager.getAllNetworkTypes()) {
+            assertNotEquals(NETWORK_TYPE_ALL, ratType)
+            assertNotEquals(NETWORK_TYPE_5G_NSA, ratType)
+        }
+    }
+
+    @Test
+    fun testOemNetworkConstants() {
+        val constantValues = arrayOf(OEM_MANAGED_YES, OEM_MANAGED_ALL, OEM_MANAGED_NO,
+                OEM_PAID, OEM_PRIVATE, OEM_PAID or OEM_PRIVATE)
+
+        // Verify that "not OEM managed network" constants are equal.
+        assertEquals(OEM_MANAGED_NO, OEM_NONE)
+
+        // Verify the constants don't conflict.
+        assertEquals(constantValues.size, constantValues.distinct().count())
+    }
+
+    /**
+     * Helper to enumerate and assert OEM managed wifi and mobile {@code NetworkTemplate}s match
+     * their the appropriate OEM managed {@code NetworkIdentity}s.
+     *
+     * @param networkType {@code TYPE_MOBILE} or {@code TYPE_WIFI}
+     * @param matchType A match rule from {@code NetworkTemplate.MATCH_*} corresponding to the
+     *         networkType.
+     * @param subscriberId To be populated with {@code TEST_IMSI*} only if networkType is
+     *         {@code TYPE_MOBILE}. May be left as null when matchType is
+     *         {@link NetworkTemplate.MATCH_MOBILE_WILDCARD}.
+     * @param templateSsid Top be populated with {@code TEST_SSID*} only if networkType is
+     *         {@code TYPE_WIFI}. May be left as null when matchType is
+     *         {@link NetworkTemplate.MATCH_WIFI_WILDCARD}.
+     * @param identSsid If networkType is {@code TYPE_WIFI}, this value must *NOT* be null. Provide
+     *         one of {@code TEST_SSID*}.
+     */
+    private fun matchOemManagedIdent(
+        networkType: Int,
+        matchType: Int,
+        subscriberId: String? = null,
+        templateSsid: String? = null,
+        identSsid: String? = null
+    ) {
+        val oemManagedStates = arrayOf(OEM_NONE, OEM_PAID, OEM_PRIVATE, OEM_PAID or OEM_PRIVATE)
+        val matchSubscriberIds = arrayOf(subscriberId)
+
+        val templateOemYes = NetworkTemplate(matchType, subscriberId, matchSubscriberIds,
+                templateSsid, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                OEM_MANAGED_YES, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+        val templateOemAll = NetworkTemplate(matchType, subscriberId, matchSubscriberIds,
+                templateSsid, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+
+        for (identityOemManagedState in oemManagedStates) {
+            val ident = buildNetworkIdentity(mockContext, buildNetworkState(networkType,
+                    subscriberId, identSsid, identityOemManagedState), /*defaultNetwork=*/false,
+                    /*subType=*/0)
+
+            // Create a template with each OEM managed type and match it against the NetworkIdentity
+            for (templateOemManagedState in oemManagedStates) {
+                val template = NetworkTemplate(matchType, subscriberId, matchSubscriberIds,
+                        templateSsid, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL,
+                        NETWORK_TYPE_ALL, templateOemManagedState, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+                if (identityOemManagedState == templateOemManagedState) {
+                    template.assertMatches(ident)
+                } else {
+                    template.assertDoesNotMatch(ident)
+                }
+            }
+            // OEM_MANAGED_ALL ignores OEM state.
+            templateOemAll.assertMatches(ident)
+            if (identityOemManagedState == OEM_NONE) {
+                // OEM_MANAGED_YES matches everything except OEM_NONE.
+                templateOemYes.assertDoesNotMatch(ident)
+            } else {
+                templateOemYes.assertMatches(ident)
+            }
+        }
+    }
+
+    @Test
+    fun testOemManagedMatchesIdent() {
+        matchOemManagedIdent(TYPE_MOBILE, MATCH_MOBILE, subscriberId = TEST_IMSI1)
+        matchOemManagedIdent(TYPE_MOBILE, MATCH_MOBILE_WILDCARD)
+        matchOemManagedIdent(TYPE_WIFI, MATCH_WIFI, templateSsid = TEST_SSID1,
+                identSsid = TEST_SSID1)
+        matchOemManagedIdent(TYPE_WIFI, MATCH_WIFI_WILDCARD, identSsid = TEST_SSID1)
+    }
+}
diff --git a/tests/unit/java/android/net/NetworkUtilsTest.java b/tests/unit/java/android/net/NetworkUtilsTest.java
new file mode 100644
index 0000000..7748288
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkUtilsTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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 junit.framework.Assert.assertEquals;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.util.TreeSet;
+
+@RunWith(AndroidJUnit4.class)
+@androidx.test.filters.SmallTest
+public class NetworkUtilsTest {
+    @Test
+    public void testRoutedIPv4AddressCount() {
+        final TreeSet<IpPrefix> set = new TreeSet<>(IpPrefix.lengthComparator());
+        // No routes routes to no addresses.
+        assertEquals(0, NetworkUtils.routedIPv4AddressCount(set));
+
+        set.add(new IpPrefix("0.0.0.0/0"));
+        assertEquals(1l << 32, NetworkUtils.routedIPv4AddressCount(set));
+
+        set.add(new IpPrefix("20.18.0.0/16"));
+        set.add(new IpPrefix("20.18.0.0/24"));
+        set.add(new IpPrefix("20.18.0.0/8"));
+        // There is a default route, still covers everything
+        assertEquals(1l << 32, NetworkUtils.routedIPv4AddressCount(set));
+
+        set.clear();
+        set.add(new IpPrefix("20.18.0.0/24"));
+        set.add(new IpPrefix("20.18.0.0/8"));
+        // The 8-length includes the 24-length prefix
+        assertEquals(1l << 24, NetworkUtils.routedIPv4AddressCount(set));
+
+        set.add(new IpPrefix("10.10.10.126/25"));
+        // The 8-length does not include this 25-length prefix
+        assertEquals((1l << 24) + (1 << 7), NetworkUtils.routedIPv4AddressCount(set));
+
+        set.clear();
+        set.add(new IpPrefix("1.2.3.4/32"));
+        set.add(new IpPrefix("1.2.3.4/32"));
+        set.add(new IpPrefix("1.2.3.4/32"));
+        set.add(new IpPrefix("1.2.3.4/32"));
+        assertEquals(1l, NetworkUtils.routedIPv4AddressCount(set));
+
+        set.add(new IpPrefix("1.2.3.5/32"));
+        set.add(new IpPrefix("1.2.3.6/32"));
+
+        set.add(new IpPrefix("1.2.3.7/32"));
+        set.add(new IpPrefix("1.2.3.8/32"));
+        set.add(new IpPrefix("1.2.3.9/32"));
+        set.add(new IpPrefix("1.2.3.0/32"));
+        assertEquals(7l, NetworkUtils.routedIPv4AddressCount(set));
+
+        // 1.2.3.4/30 eats 1.2.3.{4-7}/32
+        set.add(new IpPrefix("1.2.3.4/30"));
+        set.add(new IpPrefix("6.2.3.4/28"));
+        set.add(new IpPrefix("120.2.3.4/16"));
+        assertEquals(7l - 4 + 4 + 16 + 65536, NetworkUtils.routedIPv4AddressCount(set));
+    }
+
+    @Test
+    public void testRoutedIPv6AddressCount() {
+        final TreeSet<IpPrefix> set = new TreeSet<>(IpPrefix.lengthComparator());
+        // No routes routes to no addresses.
+        assertEquals(BigInteger.ZERO, NetworkUtils.routedIPv6AddressCount(set));
+
+        set.add(new IpPrefix("::/0"));
+        assertEquals(BigInteger.ONE.shiftLeft(128), NetworkUtils.routedIPv6AddressCount(set));
+
+        set.add(new IpPrefix("1234:622a::18/64"));
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/96"));
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/8"));
+        // There is a default route, still covers everything
+        assertEquals(BigInteger.ONE.shiftLeft(128), NetworkUtils.routedIPv6AddressCount(set));
+
+        set.clear();
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/96"));
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/8"));
+        // The 8-length includes the 96-length prefix
+        assertEquals(BigInteger.ONE.shiftLeft(120), NetworkUtils.routedIPv6AddressCount(set));
+
+        set.add(new IpPrefix("10::26/64"));
+        // The 8-length does not include this 64-length prefix
+        assertEquals(BigInteger.ONE.shiftLeft(120).add(BigInteger.ONE.shiftLeft(64)),
+                NetworkUtils.routedIPv6AddressCount(set));
+
+        set.clear();
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128"));
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128"));
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128"));
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128"));
+        assertEquals(BigInteger.ONE, NetworkUtils.routedIPv6AddressCount(set));
+
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6ad5/128"));
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6ad6/128"));
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6ad7/128"));
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6ad8/128"));
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6ad9/128"));
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6ad0/128"));
+        assertEquals(BigInteger.valueOf(7), NetworkUtils.routedIPv6AddressCount(set));
+
+        // add4:f00:80:f7:1111::6ad4/126 eats add4:f00:8[:f7:1111::6ad{4-7}/128
+        set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/126"));
+        set.add(new IpPrefix("d00d:f00:80:f7:1111::6ade/124"));
+        set.add(new IpPrefix("f00b:a33::/112"));
+        assertEquals(BigInteger.valueOf(7l - 4 + 4 + 16 + 65536),
+                NetworkUtils.routedIPv6AddressCount(set));
+    }
+}
diff --git a/tests/unit/java/android/net/QosSocketFilterTest.java b/tests/unit/java/android/net/QosSocketFilterTest.java
new file mode 100644
index 0000000..40f8f1b
--- /dev/null
+++ b/tests/unit/java/android/net/QosSocketFilterTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+
+@RunWith(AndroidJUnit4.class)
+@androidx.test.filters.SmallTest
+public class QosSocketFilterTest {
+
+    @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
+    public void testPortLessThanStart() {
+        final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
+        final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4");
+        assertFalse(QosSocketFilter.matchesAddress(
+                new InetSocketAddress(addressA, 8), addressB, 10, 10));
+    }
+
+    @Test
+    public void testPortGreaterThanEnd() {
+        final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
+        final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4");
+        assertFalse(QosSocketFilter.matchesAddress(
+                new InetSocketAddress(addressA, 18), addressB, 10, 10));
+    }
+
+    @Test
+    public void testPortBetweenStartAndEnd() {
+        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, 8, 18));
+    }
+
+    @Test
+    public void testAddressesDontMatch() {
+        final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
+        final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.5");
+        assertFalse(QosSocketFilter.matchesAddress(
+                new InetSocketAddress(addressA, 10), addressB, 10, 10));
+    }
+}
+
diff --git a/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java b/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java
new file mode 100644
index 0000000..6714bb1
--- /dev/null
+++ b/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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 static com.android.testutils.ParcelUtils.assertParcelSane;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.net.wifi.WifiNetworkSpecifier;
+import android.telephony.SubscriptionManager;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Unit test for {@link android.net.TelephonyNetworkSpecifier}.
+ */
+@SmallTest
+public class TelephonyNetworkSpecifierTest {
+    private static final int TEST_SUBID = 5;
+    private static final String TEST_SSID = "Test123";
+
+    /**
+     * Validate that IllegalArgumentException will be thrown if build TelephonyNetworkSpecifier
+     * without calling {@link TelephonyNetworkSpecifier.Builder#setSubscriptionId(int)}.
+     */
+    @Test
+    public void testBuilderBuildWithDefault() {
+        try {
+            new TelephonyNetworkSpecifier.Builder().build();
+        } catch (IllegalArgumentException iae) {
+            // expected, test pass
+        }
+    }
+
+    /**
+     * Validate that no exception will be thrown even if pass invalid subscription id to
+     * {@link TelephonyNetworkSpecifier.Builder#setSubscriptionId(int)}.
+     */
+    @Test
+    public void testBuilderBuildWithInvalidSubId() {
+        TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+                .build();
+        assertEquals(specifier.getSubscriptionId(), SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+    }
+
+    /**
+     * Validate the correctness of TelephonyNetworkSpecifier when provide valid subId.
+     */
+    @Test
+    public void testBuilderBuildWithValidSubId() {
+        final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(TEST_SUBID)
+                .build();
+        assertEquals(TEST_SUBID, specifier.getSubscriptionId());
+    }
+
+    /**
+     * Validate that parcel marshalling/unmarshalling works.
+     */
+    @Test
+    public void testParcel() {
+        TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(TEST_SUBID)
+                .build();
+        assertParcelSane(specifier, 1 /* fieldCount */);
+    }
+
+    /**
+     * Validate the behavior of method canBeSatisfiedBy().
+     */
+    @Test
+    public void testCanBeSatisfiedBy() {
+        final TelephonyNetworkSpecifier tns1 = new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(TEST_SUBID)
+                .build();
+        final TelephonyNetworkSpecifier tns2 = new TelephonyNetworkSpecifier.Builder()
+                .setSubscriptionId(TEST_SUBID)
+                .build();
+        final WifiNetworkSpecifier wns = new WifiNetworkSpecifier.Builder()
+                .setSsid(TEST_SSID)
+                .build();
+        final MatchAllNetworkSpecifier mans = new MatchAllNetworkSpecifier();
+
+        // Test equality
+        assertEquals(tns1, tns2);
+        assertTrue(tns1.canBeSatisfiedBy(tns1));
+        assertTrue(tns1.canBeSatisfiedBy(tns2));
+
+        // Test other edge cases.
+        assertFalse(tns1.canBeSatisfiedBy(null));
+        assertFalse(tns1.canBeSatisfiedBy(wns));
+        assertTrue(tns1.canBeSatisfiedBy(mans));
+    }
+}
diff --git a/tests/unit/java/android/net/VpnManagerTest.java b/tests/unit/java/android/net/VpnManagerTest.java
new file mode 100644
index 0000000..3135062
--- /dev/null
+++ b/tests/unit/java/android/net/VpnManagerTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.test.mock.MockContext;
+import android.util.SparseArray;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.net.VpnProfile;
+import com.android.internal.util.MessageUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link VpnManager}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class VpnManagerTest {
+    private static final String PKG_NAME = "fooPackage";
+
+    private static final String SESSION_NAME_STRING = "testSession";
+    private static final String SERVER_ADDR_STRING = "1.2.3.4";
+    private static final String IDENTITY_STRING = "Identity";
+    private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
+
+    private IVpnManager mMockService;
+    private VpnManager mVpnManager;
+    private final MockContext mMockContext =
+            new MockContext() {
+                @Override
+                public String getOpPackageName() {
+                    return PKG_NAME;
+                }
+            };
+
+    @Before
+    public void setUp() throws Exception {
+        mMockService = mock(IVpnManager.class);
+        mVpnManager = new VpnManager(mMockContext, mMockService);
+    }
+
+    @Test
+    public void testProvisionVpnProfilePreconsented() throws Exception {
+        final PlatformVpnProfile profile = getPlatformVpnProfile();
+        when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME)))
+                .thenReturn(true);
+
+        // Expect there to be no intent returned, as consent has already been granted.
+        assertNull(mVpnManager.provisionVpnProfile(profile));
+        verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
+    }
+
+    @Test
+    public void testProvisionVpnProfileNeedsConsent() throws Exception {
+        final PlatformVpnProfile profile = getPlatformVpnProfile();
+        when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME)))
+                .thenReturn(false);
+
+        // Expect intent to be returned, as consent has not already been granted.
+        final Intent intent = mVpnManager.provisionVpnProfile(profile);
+        assertNotNull(intent);
+
+        final ComponentName expectedComponentName =
+                ComponentName.unflattenFromString(
+                        "com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog");
+        assertEquals(expectedComponentName, intent.getComponent());
+        verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
+    }
+
+    @Test
+    public void testDeleteProvisionedVpnProfile() throws Exception {
+        mVpnManager.deleteProvisionedVpnProfile();
+        verify(mMockService).deleteVpnProfile(eq(PKG_NAME));
+    }
+
+    @Test
+    public void testStartProvisionedVpnProfile() throws Exception {
+        mVpnManager.startProvisionedVpnProfile();
+        verify(mMockService).startVpnProfile(eq(PKG_NAME));
+    }
+
+    @Test
+    public void testStopProvisionedVpnProfile() throws Exception {
+        mVpnManager.stopProvisionedVpnProfile();
+        verify(mMockService).stopVpnProfile(eq(PKG_NAME));
+    }
+
+    private Ikev2VpnProfile getPlatformVpnProfile() throws Exception {
+        return new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
+                .setBypassable(true)
+                .setMaxMtu(1300)
+                .setMetered(true)
+                .setAuthPsk(PSK_BYTES)
+                .build();
+    }
+
+    @Test
+    public void testVpnTypesEqual() throws Exception {
+        SparseArray<String> vmVpnTypes = MessageUtils.findMessageNames(
+                new Class[] { VpnManager.class }, new String[]{ "TYPE_VPN_" });
+        SparseArray<String> nativeVpnType = MessageUtils.findMessageNames(
+                new Class[] { NativeVpnType.class }, new String[]{ "" });
+
+        // TYPE_VPN_NONE = -1 is only defined in VpnManager.
+        assertEquals(vmVpnTypes.size() - 1, nativeVpnType.size());
+        for (int i = VpnManager.TYPE_VPN_SERVICE; i < vmVpnTypes.size(); i++) {
+            assertEquals(vmVpnTypes.get(i), "TYPE_VPN_" + nativeVpnType.get(i));
+        }
+    }
+}
diff --git a/tests/unit/java/android/net/VpnTransportInfoTest.java b/tests/unit/java/android/net/VpnTransportInfoTest.java
new file mode 100644
index 0000000..ccaa5cf
--- /dev/null
+++ b/tests/unit/java/android/net/VpnTransportInfoTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
+import static android.net.NetworkCapabilities.REDACT_NONE;
+
+import static com.android.testutils.ParcelUtils.assertParcelSane;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VpnTransportInfoTest {
+
+    @Test
+    public void testParceling() {
+        VpnTransportInfo v = new VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, "12345");
+        assertParcelSane(v, 2 /* fieldCount */);
+    }
+
+    @Test
+    public void testEqualsAndHashCode() {
+        String session1 = "12345";
+        String session2 = "6789";
+        VpnTransportInfo v11 = new VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, session1);
+        VpnTransportInfo v12 = new VpnTransportInfo(VpnManager.TYPE_VPN_SERVICE, session1);
+        VpnTransportInfo v13 = new VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, session1);
+        VpnTransportInfo v14 = new VpnTransportInfo(VpnManager.TYPE_VPN_LEGACY, session1);
+        VpnTransportInfo v15 = new VpnTransportInfo(VpnManager.TYPE_VPN_OEM, session1);
+        VpnTransportInfo v21 = new VpnTransportInfo(VpnManager.TYPE_VPN_LEGACY, session2);
+
+        VpnTransportInfo v31 = v11.makeCopy(REDACT_FOR_NETWORK_SETTINGS);
+        VpnTransportInfo v32 = v13.makeCopy(REDACT_FOR_NETWORK_SETTINGS);
+
+        assertNotEquals(v11, v12);
+        assertNotEquals(v13, v14);
+        assertNotEquals(v14, v15);
+        assertNotEquals(v14, v21);
+
+        assertEquals(v11, v13);
+        assertEquals(v31, v32);
+        assertEquals(v11.hashCode(), v13.hashCode());
+        assertEquals(REDACT_FOR_NETWORK_SETTINGS, v32.getApplicableRedactions());
+        assertEquals(session1, v15.makeCopy(REDACT_NONE).getSessionId());
+    }
+}
diff --git a/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java b/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java
new file mode 100644
index 0000000..603c875
--- /dev/null
+++ b/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java
@@ -0,0 +1,142 @@
+/*
+ * 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.ipmemorystore;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirk;
+import android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirkParcelable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Modifier;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.Collections;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ParcelableTests {
+    @Test
+    public void testNetworkAttributesParceling() throws Exception {
+        final NetworkAttributes.Builder builder = new NetworkAttributes.Builder();
+        NetworkAttributes in = builder.build();
+        assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable())));
+
+        builder.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4"));
+        // lease will expire in two hours
+        builder.setAssignedV4AddressExpiry(System.currentTimeMillis() + 7_200_000);
+        // cluster stays null this time around
+        builder.setDnsAddresses(Collections.emptyList());
+        builder.setMtu(18);
+        in = builder.build();
+        assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable())));
+
+        builder.setAssignedV4Address((Inet4Address) Inet4Address.getByName("6.7.8.9"));
+        builder.setAssignedV4AddressExpiry(System.currentTimeMillis() + 3_600_000);
+        builder.setCluster("groupHint");
+        builder.setDnsAddresses(Arrays.asList(
+                InetAddress.getByName("ACA1:652B:0911:DE8F:1200:115E:913B:AA2A"),
+                InetAddress.getByName("6.7.8.9")));
+        builder.setMtu(1_000_000);
+        in = builder.build();
+        assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable())));
+
+        builder.setMtu(null);
+        in = builder.build();
+        assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable())));
+
+        // Verify that this test does not miss any new field added later.
+        // If any field is added to NetworkAttributes it must be tested here for parceling
+        // roundtrip.
+        assertEquals(6, Arrays.stream(NetworkAttributes.class.getDeclaredFields())
+                .filter(f -> !Modifier.isStatic(f.getModifiers())).count());
+    }
+
+    @Test
+    public void testPrivateDataParceling() throws Exception {
+        final Blob in = new Blob();
+        in.data = new byte[] {89, 111, 108, 111};
+        final Blob out = parcelingRoundTrip(in);
+        // Object.equals on byte[] tests the references
+        assertEquals(in.data.length, out.data.length);
+        assertTrue(Arrays.equals(in.data, out.data));
+    }
+
+    @Test
+    public void testSameL3NetworkResponseParceling() throws Exception {
+        final SameL3NetworkResponseParcelable parcelable = new SameL3NetworkResponseParcelable();
+        parcelable.l2Key1 = "key 1";
+        parcelable.l2Key2 = "key 2";
+        parcelable.confidence = 0.43f;
+
+        final SameL3NetworkResponse in = new SameL3NetworkResponse(parcelable);
+        assertEquals("key 1", in.l2Key1);
+        assertEquals("key 2", in.l2Key2);
+        assertEquals(0.43f, in.confidence, 0.01f /* delta */);
+
+        final SameL3NetworkResponse out =
+                new SameL3NetworkResponse(parcelingRoundTrip(in.toParcelable()));
+
+        assertEquals(in, out);
+        assertEquals(in.l2Key1, out.l2Key1);
+        assertEquals(in.l2Key2, out.l2Key2);
+        assertEquals(in.confidence, out.confidence, 0.01f /* delta */);
+    }
+
+    @Test
+    public void testIPv6ProvisioningLossQuirkParceling() throws Exception {
+        final NetworkAttributes.Builder builder = new NetworkAttributes.Builder();
+        final IPv6ProvisioningLossQuirkParcelable parcelable =
+                new IPv6ProvisioningLossQuirkParcelable();
+        final long expiry = System.currentTimeMillis() + 7_200_000;
+
+        parcelable.detectionCount = 3;
+        parcelable.quirkExpiry = expiry; // quirk info will expire in two hours
+        builder.setIpv6ProvLossQuirk(IPv6ProvisioningLossQuirk.fromStableParcelable(parcelable));
+        final NetworkAttributes in = builder.build();
+
+        final NetworkAttributes out = new NetworkAttributes(parcelingRoundTrip(in.toParcelable()));
+        assertEquals(out.ipv6ProvisioningLossQuirk, in.ipv6ProvisioningLossQuirk);
+    }
+
+    private <T extends Parcelable> T parcelingRoundTrip(final T in) throws Exception {
+        final Parcel p = Parcel.obtain();
+        in.writeToParcel(p, /* flags */ 0);
+        p.setDataPosition(0);
+        final byte[] marshalledData = p.marshall();
+        p.recycle();
+
+        final Parcel q = Parcel.obtain();
+        q.unmarshall(marshalledData, 0, marshalledData.length);
+        q.setDataPosition(0);
+
+        final Parcelable.Creator<T> creator = (Parcelable.Creator<T>)
+                in.getClass().getField("CREATOR").get(null); // static object, so null receiver
+        final T unmarshalled = (T) creator.createFromParcel(q);
+        q.recycle();
+        return unmarshalled;
+    }
+}
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
new file mode 100644
index 0000000..370179c
--- /dev/null
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -0,0 +1,388 @@
+/*
+ * 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.nsd;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+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 android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.AsyncChannel;
+import com.android.testutils.HandlerUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NsdManagerTest {
+
+    static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
+
+    @Mock Context mContext;
+    @Mock INsdManager mService;
+    MockServiceHandler mServiceHandler;
+
+    NsdManager mManager;
+
+    long mTimeoutMs = 200; // non-final so that tests can adjust the value.
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mServiceHandler = spy(MockServiceHandler.create(mContext));
+        doReturn(new Messenger(mServiceHandler)).when(mService).getMessenger();
+
+        mManager = makeManager();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        HandlerUtils.waitForIdle(mServiceHandler, mTimeoutMs);
+        mServiceHandler.chan.disconnect();
+        mServiceHandler.stop();
+        if (mManager != null) {
+            mManager.disconnect();
+        }
+    }
+
+    @Test
+    public void testResolveService() {
+        NsdManager manager = mManager;
+
+        NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
+        NsdServiceInfo reply = new NsdServiceInfo("resolved_name", "resolved_type");
+        NsdManager.ResolveListener listener = mock(NsdManager.ResolveListener.class);
+
+        manager.resolveService(request, listener);
+        int key1 = verifyRequest(NsdManager.RESOLVE_SERVICE);
+        int err = 33;
+        sendResponse(NsdManager.RESOLVE_SERVICE_FAILED, err, key1, null);
+        verify(listener, timeout(mTimeoutMs).times(1)).onResolveFailed(request, err);
+
+        manager.resolveService(request, listener);
+        int key2 = verifyRequest(NsdManager.RESOLVE_SERVICE);
+        sendResponse(NsdManager.RESOLVE_SERVICE_SUCCEEDED, 0, key2, reply);
+        verify(listener, timeout(mTimeoutMs).times(1)).onServiceResolved(reply);
+    }
+
+    @Test
+    public void testParallelResolveService() {
+        NsdManager manager = mManager;
+
+        NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
+        NsdServiceInfo reply = new NsdServiceInfo("resolved_name", "resolved_type");
+
+        NsdManager.ResolveListener listener1 = mock(NsdManager.ResolveListener.class);
+        NsdManager.ResolveListener listener2 = mock(NsdManager.ResolveListener.class);
+
+        manager.resolveService(request, listener1);
+        int key1 = verifyRequest(NsdManager.RESOLVE_SERVICE);
+
+        manager.resolveService(request, listener2);
+        int key2 = verifyRequest(NsdManager.RESOLVE_SERVICE);
+
+        sendResponse(NsdManager.RESOLVE_SERVICE_SUCCEEDED, 0, key2, reply);
+        sendResponse(NsdManager.RESOLVE_SERVICE_SUCCEEDED, 0, key1, reply);
+
+        verify(listener1, timeout(mTimeoutMs).times(1)).onServiceResolved(reply);
+        verify(listener2, timeout(mTimeoutMs).times(1)).onServiceResolved(reply);
+    }
+
+    @Test
+    public void testRegisterService() {
+        NsdManager manager = mManager;
+
+        NsdServiceInfo request1 = new NsdServiceInfo("a_name", "a_type");
+        NsdServiceInfo request2 = new NsdServiceInfo("another_name", "another_type");
+        request1.setPort(2201);
+        request2.setPort(2202);
+        NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
+        NsdManager.RegistrationListener listener2 = mock(NsdManager.RegistrationListener.class);
+
+        // Register two services
+        manager.registerService(request1, PROTOCOL, listener1);
+        int key1 = verifyRequest(NsdManager.REGISTER_SERVICE);
+
+        manager.registerService(request2, PROTOCOL, listener2);
+        int key2 = verifyRequest(NsdManager.REGISTER_SERVICE);
+
+        // First reques fails, second request succeeds
+        sendResponse(NsdManager.REGISTER_SERVICE_SUCCEEDED, 0, key2, request2);
+        verify(listener2, timeout(mTimeoutMs).times(1)).onServiceRegistered(request2);
+
+        int err = 1;
+        sendResponse(NsdManager.REGISTER_SERVICE_FAILED, err, key1, request1);
+        verify(listener1, timeout(mTimeoutMs).times(1)).onRegistrationFailed(request1, err);
+
+        // Client retries first request, it succeeds
+        manager.registerService(request1, PROTOCOL, listener1);
+        int key3 = verifyRequest(NsdManager.REGISTER_SERVICE);
+
+        sendResponse(NsdManager.REGISTER_SERVICE_SUCCEEDED, 0, key3, request1);
+        verify(listener1, timeout(mTimeoutMs).times(1)).onServiceRegistered(request1);
+
+        // First request is unregistered, it succeeds
+        manager.unregisterService(listener1);
+        int key3again = verifyRequest(NsdManager.UNREGISTER_SERVICE);
+        assertEquals(key3, key3again);
+
+        sendResponse(NsdManager.UNREGISTER_SERVICE_SUCCEEDED, 0, key3again, null);
+        verify(listener1, timeout(mTimeoutMs).times(1)).onServiceUnregistered(request1);
+
+        // Second request is unregistered, it fails
+        manager.unregisterService(listener2);
+        int key2again = verifyRequest(NsdManager.UNREGISTER_SERVICE);
+        assertEquals(key2, key2again);
+
+        sendResponse(NsdManager.UNREGISTER_SERVICE_FAILED, err, key2again, null);
+        verify(listener2, timeout(mTimeoutMs).times(1)).onUnregistrationFailed(request2, err);
+
+        // TODO: do not unregister listener until service is unregistered
+        // Client retries unregistration of second request, it succeeds
+        //manager.unregisterService(listener2);
+        //int key2yetAgain = verifyRequest(NsdManager.UNREGISTER_SERVICE);
+        //assertEquals(key2, key2yetAgain);
+
+        //sendResponse(NsdManager.UNREGISTER_SERVICE_SUCCEEDED, 0, key2yetAgain, null);
+        //verify(listener2, timeout(mTimeoutMs).times(1)).onServiceUnregistered(request2);
+    }
+
+    @Test
+    public void testDiscoverService() {
+        NsdManager manager = mManager;
+
+        NsdServiceInfo reply1 = new NsdServiceInfo("a_name", "a_type");
+        NsdServiceInfo reply2 = new NsdServiceInfo("another_name", "a_type");
+        NsdServiceInfo reply3 = new NsdServiceInfo("a_third_name", "a_type");
+
+        NsdManager.DiscoveryListener listener = mock(NsdManager.DiscoveryListener.class);
+
+        // Client registers for discovery, request fails
+        manager.discoverServices("a_type", PROTOCOL, listener);
+        int key1 = verifyRequest(NsdManager.DISCOVER_SERVICES);
+
+        int err = 1;
+        sendResponse(NsdManager.DISCOVER_SERVICES_FAILED, err, key1, null);
+        verify(listener, timeout(mTimeoutMs).times(1)).onStartDiscoveryFailed("a_type", err);
+
+        // Client retries, request succeeds
+        manager.discoverServices("a_type", PROTOCOL, listener);
+        int key2 = verifyRequest(NsdManager.DISCOVER_SERVICES);
+
+        sendResponse(NsdManager.DISCOVER_SERVICES_STARTED, 0, key2, reply1);
+        verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type");
+
+
+        // mdns notifies about services
+        sendResponse(NsdManager.SERVICE_FOUND, 0, key2, reply1);
+        verify(listener, timeout(mTimeoutMs).times(1)).onServiceFound(reply1);
+
+        sendResponse(NsdManager.SERVICE_FOUND, 0, key2, reply2);
+        verify(listener, timeout(mTimeoutMs).times(1)).onServiceFound(reply2);
+
+        sendResponse(NsdManager.SERVICE_LOST, 0, key2, reply2);
+        verify(listener, timeout(mTimeoutMs).times(1)).onServiceLost(reply2);
+
+
+        // Client unregisters its listener
+        manager.stopServiceDiscovery(listener);
+        int key2again = verifyRequest(NsdManager.STOP_DISCOVERY);
+        assertEquals(key2, key2again);
+
+        // TODO: unregister listener immediately and stop notifying it about services
+        // Notifications are still passed to the client's listener
+        sendResponse(NsdManager.SERVICE_LOST, 0, key2, reply1);
+        verify(listener, timeout(mTimeoutMs).times(1)).onServiceLost(reply1);
+
+        // Client is notified of complete unregistration
+        sendResponse(NsdManager.STOP_DISCOVERY_SUCCEEDED, 0, key2again, "a_type");
+        verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStopped("a_type");
+
+        // Notifications are not passed to the client anymore
+        sendResponse(NsdManager.SERVICE_FOUND, 0, key2, reply3);
+        verify(listener, timeout(mTimeoutMs).times(0)).onServiceLost(reply3);
+
+
+        // Client registers for service discovery
+        reset(listener);
+        manager.discoverServices("a_type", PROTOCOL, listener);
+        int key3 = verifyRequest(NsdManager.DISCOVER_SERVICES);
+
+        sendResponse(NsdManager.DISCOVER_SERVICES_STARTED, 0, key3, reply1);
+        verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type");
+
+        // Client unregisters immediately, it fails
+        manager.stopServiceDiscovery(listener);
+        int key3again = verifyRequest(NsdManager.STOP_DISCOVERY);
+        assertEquals(key3, key3again);
+
+        err = 2;
+        sendResponse(NsdManager.STOP_DISCOVERY_FAILED, err, key3again, "a_type");
+        verify(listener, timeout(mTimeoutMs).times(1)).onStopDiscoveryFailed("a_type", err);
+
+        // New notifications are not passed to the client anymore
+        sendResponse(NsdManager.SERVICE_FOUND, 0, key3, reply1);
+        verify(listener, timeout(mTimeoutMs).times(0)).onServiceFound(reply1);
+    }
+
+    @Test
+    public void testInvalidCalls() {
+        NsdManager manager = mManager;
+
+        NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
+        NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
+        NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
+
+        NsdServiceInfo invalidService = new NsdServiceInfo(null, null);
+        NsdServiceInfo validService = new NsdServiceInfo("a_name", "a_type");
+        validService.setPort(2222);
+
+        // Service registration
+        //  - invalid arguments
+        mustFail(() -> { manager.unregisterService(null); });
+        mustFail(() -> { manager.registerService(null, -1, null); });
+        mustFail(() -> { manager.registerService(null, PROTOCOL, listener1); });
+        mustFail(() -> { manager.registerService(invalidService, PROTOCOL, listener1); });
+        mustFail(() -> { manager.registerService(validService, -1, listener1); });
+        mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
+        manager.registerService(validService, PROTOCOL, listener1);
+        //  - listener already registered
+        mustFail(() -> { manager.registerService(validService, PROTOCOL, listener1); });
+        manager.unregisterService(listener1);
+        // TODO: make listener immediately reusable
+        //mustFail(() -> { manager.unregisterService(listener1); });
+        //manager.registerService(validService, PROTOCOL, listener1);
+
+        // Discover service
+        //  - invalid arguments
+        mustFail(() -> { manager.stopServiceDiscovery(null); });
+        mustFail(() -> { manager.discoverServices(null, -1, null); });
+        mustFail(() -> { manager.discoverServices(null, PROTOCOL, listener2); });
+        mustFail(() -> { manager.discoverServices("a_service", -1, listener2); });
+        mustFail(() -> { manager.discoverServices("a_service", PROTOCOL, null); });
+        manager.discoverServices("a_service", PROTOCOL, listener2);
+        //  - listener already registered
+        mustFail(() -> { manager.discoverServices("another_service", PROTOCOL, listener2); });
+        manager.stopServiceDiscovery(listener2);
+        // TODO: make listener immediately reusable
+        //mustFail(() -> { manager.stopServiceDiscovery(listener2); });
+        //manager.discoverServices("another_service", PROTOCOL, listener2);
+
+        // Resolver service
+        //  - invalid arguments
+        mustFail(() -> { manager.resolveService(null, null); });
+        mustFail(() -> { manager.resolveService(null, listener3); });
+        mustFail(() -> { manager.resolveService(invalidService, listener3); });
+        mustFail(() -> { manager.resolveService(validService, null); });
+        manager.resolveService(validService, listener3);
+        //  - listener already registered:w
+        mustFail(() -> { manager.resolveService(validService, listener3); });
+    }
+
+    public void mustFail(Runnable fn) {
+        try {
+            fn.run();
+            fail();
+        } catch (Exception expected) {
+        }
+    }
+
+    NsdManager makeManager() {
+        NsdManager manager = new NsdManager(mContext, mService);
+        // Acknowledge first two messages connecting the AsyncChannel.
+        verify(mServiceHandler, timeout(mTimeoutMs).times(2)).handleMessage(any());
+        reset(mServiceHandler);
+        assertNotNull(mServiceHandler.chan);
+        return manager;
+    }
+
+    int verifyRequest(int expectedMessageType) {
+        HandlerUtils.waitForIdle(mServiceHandler, mTimeoutMs);
+        verify(mServiceHandler, timeout(mTimeoutMs)).handleMessage(any());
+        reset(mServiceHandler);
+        Message received = mServiceHandler.getLastMessage();
+        assertEquals(NsdManager.nameOf(expectedMessageType), NsdManager.nameOf(received.what));
+        return received.arg2;
+    }
+
+    void sendResponse(int replyType, int arg, int key, Object obj) {
+        mServiceHandler.chan.sendMessage(replyType, arg, key, obj);
+    }
+
+    // Implements the server side of AsyncChannel connection protocol
+    public static class MockServiceHandler extends Handler {
+        public final Context context;
+        public AsyncChannel chan;
+        public Message lastMessage;
+
+        MockServiceHandler(Looper l, Context c) {
+            super(l);
+            context = c;
+        }
+
+        synchronized Message getLastMessage() {
+            return lastMessage;
+        }
+
+        synchronized void setLastMessage(Message msg) {
+            lastMessage = obtainMessage();
+            lastMessage.copyFrom(msg);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            setLastMessage(msg);
+            if (msg.what == AsyncChannel.CMD_CHANNEL_FULL_CONNECTION) {
+                chan = new AsyncChannel();
+                chan.connect(context, this, msg.replyTo);
+                chan.sendMessage(AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED);
+            }
+        }
+
+        void stop() {
+            getLooper().quitSafely();
+        }
+
+        static MockServiceHandler create(Context context) {
+            HandlerThread t = new HandlerThread("mock-service-handler");
+            t.start();
+            return new MockServiceHandler(t.getLooper(), context);
+        }
+    }
+}
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
new file mode 100644
index 0000000..94dfc75
--- /dev/null
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.nsd;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.StrictMode;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NsdServiceInfoTest {
+
+    public final static InetAddress LOCALHOST;
+    static {
+        // Because test.
+        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
+        StrictMode.setThreadPolicy(policy);
+
+        InetAddress _host = null;
+        try {
+            _host = InetAddress.getLocalHost();
+        } catch (UnknownHostException e) { }
+        LOCALHOST = _host;
+    }
+
+    @Test
+    public void testLimits() throws Exception {
+        NsdServiceInfo info = new NsdServiceInfo();
+
+        // Non-ASCII keys.
+        boolean exceptionThrown = false;
+        try {
+            info.setAttribute("猫", "meow");
+        } catch (IllegalArgumentException e) {
+            exceptionThrown = true;
+        }
+        assertTrue(exceptionThrown);
+        assertEmptyServiceInfo(info);
+
+        // ASCII keys with '=' character.
+        exceptionThrown = false;
+        try {
+            info.setAttribute("kitten=", "meow");
+        } catch (IllegalArgumentException e) {
+            exceptionThrown = true;
+        }
+        assertTrue(exceptionThrown);
+        assertEmptyServiceInfo(info);
+
+        // Single key + value length too long.
+        exceptionThrown = false;
+        try {
+            String longValue = "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" +
+                    "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" +
+                    "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" +
+                    "ooooooooooooooooooooooooooooong";  // 248 characters.
+            info.setAttribute("longcat", longValue);  // Key + value == 255 characters.
+        } catch (IllegalArgumentException e) {
+            exceptionThrown = true;
+        }
+        assertTrue(exceptionThrown);
+        assertEmptyServiceInfo(info);
+
+        // Total TXT record length too long.
+        exceptionThrown = false;
+        int recordsAdded = 0;
+        try {
+            for (int i = 100; i < 300; ++i) {
+                // 6 char key + 5 char value + 2 bytes overhead = 13 byte record length.
+                String key = String.format("key%d", i);
+                info.setAttribute(key, "12345");
+                recordsAdded++;
+            }
+        } catch (IllegalArgumentException e) {
+            exceptionThrown = true;
+        }
+        assertTrue(exceptionThrown);
+        assertTrue(100 == recordsAdded);
+        assertTrue(info.getTxtRecord().length == 1300);
+    }
+
+    @Test
+    public void testParcel() throws Exception {
+        NsdServiceInfo emptyInfo = new NsdServiceInfo();
+        checkParcelable(emptyInfo);
+
+        NsdServiceInfo fullInfo = new NsdServiceInfo();
+        fullInfo.setServiceName("kitten");
+        fullInfo.setServiceType("_kitten._tcp");
+        fullInfo.setPort(4242);
+        fullInfo.setHost(LOCALHOST);
+        checkParcelable(fullInfo);
+
+        NsdServiceInfo noHostInfo = new NsdServiceInfo();
+        noHostInfo.setServiceName("kitten");
+        noHostInfo.setServiceType("_kitten._tcp");
+        noHostInfo.setPort(4242);
+        checkParcelable(noHostInfo);
+
+        NsdServiceInfo attributedInfo = new NsdServiceInfo();
+        attributedInfo.setServiceName("kitten");
+        attributedInfo.setServiceType("_kitten._tcp");
+        attributedInfo.setPort(4242);
+        attributedInfo.setHost(LOCALHOST);
+        attributedInfo.setAttribute("color", "pink");
+        attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
+        attributedInfo.setAttribute("adorable", (String) null);
+        attributedInfo.setAttribute("sticky", "yes");
+        attributedInfo.setAttribute("siblings", new byte[] {});
+        attributedInfo.setAttribute("edge cases", new byte[] {0, -1, 127, -128});
+        attributedInfo.removeAttribute("sticky");
+        checkParcelable(attributedInfo);
+
+        // Sanity check that we actually wrote attributes to attributedInfo.
+        assertTrue(attributedInfo.getAttributes().keySet().contains("adorable"));
+        String sound = new String(attributedInfo.getAttributes().get("sound"), "UTF-8");
+        assertTrue(sound.equals("にゃあ"));
+        byte[] edgeCases = attributedInfo.getAttributes().get("edge cases");
+        assertTrue(Arrays.equals(edgeCases, new byte[] {0, -1, 127, -128}));
+        assertFalse(attributedInfo.getAttributes().keySet().contains("sticky"));
+    }
+
+    public void checkParcelable(NsdServiceInfo original) {
+        // Write to parcel.
+        Parcel p = Parcel.obtain();
+        Bundle writer = new Bundle();
+        writer.putParcelable("test_info", original);
+        writer.writeToParcel(p, 0);
+
+        // Extract from parcel.
+        p.setDataPosition(0);
+        Bundle reader = p.readBundle();
+        reader.setClassLoader(NsdServiceInfo.class.getClassLoader());
+        NsdServiceInfo result = reader.getParcelable("test_info");
+
+        // Assert equality of base fields.
+        assertEquals(original.getServiceName(), result.getServiceName());
+        assertEquals(original.getServiceType(), result.getServiceType());
+        assertEquals(original.getHost(), result.getHost());
+        assertTrue(original.getPort() == result.getPort());
+
+        // Assert equality of attribute map.
+        Map<String, byte[]> originalMap = original.getAttributes();
+        Map<String, byte[]> resultMap = result.getAttributes();
+        assertEquals(originalMap.keySet(), resultMap.keySet());
+        for (String key : originalMap.keySet()) {
+            assertTrue(Arrays.equals(originalMap.get(key), resultMap.get(key)));
+        }
+    }
+
+    public void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
+        byte[] txtRecord = shouldBeEmpty.getTxtRecord();
+        if (txtRecord == null || txtRecord.length == 0) {
+            return;
+        }
+        fail("NsdServiceInfo.getTxtRecord did not return null but " + Arrays.toString(txtRecord));
+    }
+}
diff --git a/tests/unit/java/android/net/util/DnsUtilsTest.java b/tests/unit/java/android/net/util/DnsUtilsTest.java
new file mode 100644
index 0000000..b626db8
--- /dev/null
+++ b/tests/unit/java/android/net/util/DnsUtilsTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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.util;
+
+import static android.net.util.DnsUtils.IPV6_ADDR_SCOPE_GLOBAL;
+import static android.net.util.DnsUtils.IPV6_ADDR_SCOPE_LINKLOCAL;
+import static android.net.util.DnsUtils.IPV6_ADDR_SCOPE_SITELOCAL;
+
+import static org.junit.Assert.assertEquals;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.InetAddresses;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DnsUtilsTest {
+    private InetAddress stringToAddress(@NonNull String addr) {
+        return InetAddresses.parseNumericAddress(addr);
+    }
+
+    private DnsUtils.SortableAddress makeSortableAddress(@NonNull String addr) {
+        return makeSortableAddress(addr, null);
+    }
+
+    private DnsUtils.SortableAddress makeSortableAddress(@NonNull String addr,
+            @Nullable String srcAddr) {
+        return new DnsUtils.SortableAddress(stringToAddress(addr),
+                srcAddr != null ? stringToAddress(srcAddr) : null);
+    }
+
+    @Test
+    public void testRfc6724Comparator() {
+        final List<DnsUtils.SortableAddress> test = Arrays.asList(
+                // Ipv4
+                makeSortableAddress("216.58.200.36", "192.168.1.1"),
+                // global with different scope src
+                makeSortableAddress("2404:6800:4008:801::2004", "fe80::1111:2222"),
+                // global without src addr
+                makeSortableAddress("2404:6800:cafe:801::1"),
+                // loop back
+                makeSortableAddress("::1", "::1"),
+                // link local
+                makeSortableAddress("fe80::c46f:1cff:fe04:39b4", "fe80::1"),
+                // teredo tunneling
+                makeSortableAddress("2001::47c1", "2001::2"),
+                // 6bone without src addr
+                makeSortableAddress("3ffe::1234:5678"),
+                // IPv4-compatible
+                makeSortableAddress("::216.58.200.36", "::216.58.200.9"),
+                // 6bone
+                makeSortableAddress("3ffe::1234:5678", "3ffe::1234:1"),
+                // IPv4-mapped IPv6
+                makeSortableAddress("::ffff:192.168.95.7", "::ffff:192.168.95.1"));
+
+        final List<InetAddress> expected = Arrays.asList(
+                stringToAddress("::1"),                       // loop back
+                stringToAddress("fe80::c46f:1cff:fe04:39b4"), // link local
+                stringToAddress("216.58.200.36"),             // Ipv4
+                stringToAddress("::ffff:192.168.95.7"),       // IPv4-mapped IPv6
+                stringToAddress("2001::47c1"),                // teredo tunneling
+                stringToAddress("::216.58.200.36"),           // IPv4-compatible
+                stringToAddress("3ffe::1234:5678"),           // 6bone
+                stringToAddress("2404:6800:4008:801::2004"),  // global with different scope src
+                stringToAddress("2404:6800:cafe:801::1"),     // global without src addr
+                stringToAddress("3ffe::1234:5678"));          // 6bone without src addr
+
+        Collections.sort(test, new DnsUtils.Rfc6724Comparator());
+
+        for (int i = 0; i < test.size(); ++i) {
+            assertEquals(test.get(i).address, expected.get(i));
+        }
+
+        // TODO: add more combinations
+    }
+
+    @Test
+    public void testV4SortableAddress() {
+        // Test V4 address
+        DnsUtils.SortableAddress test = makeSortableAddress("216.58.200.36");
+        assertEquals(test.hasSrcAddr, 0);
+        assertEquals(test.prefixMatchLen, 0);
+        assertEquals(test.address, stringToAddress("216.58.200.36"));
+        assertEquals(test.labelMatch, 0);
+        assertEquals(test.scopeMatch, 0);
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL);
+        assertEquals(test.label, 4);
+        assertEquals(test.precedence, 35);
+
+        // Test V4 loopback address with the same source address
+        test = makeSortableAddress("127.1.2.3", "127.1.2.3");
+        assertEquals(test.hasSrcAddr, 1);
+        assertEquals(test.prefixMatchLen, 0);
+        assertEquals(test.address, stringToAddress("127.1.2.3"));
+        assertEquals(test.labelMatch, 1);
+        assertEquals(test.scopeMatch, 1);
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_LINKLOCAL);
+        assertEquals(test.label, 4);
+        assertEquals(test.precedence, 35);
+    }
+
+    @Test
+    public void testV6SortableAddress() {
+        // Test global address
+        DnsUtils.SortableAddress test = makeSortableAddress("2404:6800:4008:801::2004");
+        assertEquals(test.address, stringToAddress("2404:6800:4008:801::2004"));
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL);
+        assertEquals(test.label, 1);
+        assertEquals(test.precedence, 40);
+
+        // Test global address with global source address
+        test = makeSortableAddress("2404:6800:4008:801::2004",
+                "2401:fa00:fc:fd00:6d6c:7199:b8e7:41d6");
+        assertEquals(test.address, stringToAddress("2404:6800:4008:801::2004"));
+        assertEquals(test.hasSrcAddr, 1);
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL);
+        assertEquals(test.labelMatch, 1);
+        assertEquals(test.scopeMatch, 1);
+        assertEquals(test.label, 1);
+        assertEquals(test.precedence, 40);
+        assertEquals(test.prefixMatchLen, 13);
+
+        // Test global address with linklocal source address
+        test = makeSortableAddress("2404:6800:4008:801::2004", "fe80::c46f:1cff:fe04:39b4");
+        assertEquals(test.hasSrcAddr, 1);
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL);
+        assertEquals(test.labelMatch, 1);
+        assertEquals(test.scopeMatch, 0);
+        assertEquals(test.label, 1);
+        assertEquals(test.precedence, 40);
+        assertEquals(test.prefixMatchLen, 0);
+
+        // Test loopback address with the same source address
+        test = makeSortableAddress("::1", "::1");
+        assertEquals(test.hasSrcAddr, 1);
+        assertEquals(test.prefixMatchLen, 16 * 8);
+        assertEquals(test.labelMatch, 1);
+        assertEquals(test.scopeMatch, 1);
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_LINKLOCAL);
+        assertEquals(test.label, 0);
+        assertEquals(test.precedence, 50);
+
+        // Test linklocal address
+        test = makeSortableAddress("fe80::c46f:1cff:fe04:39b4");
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_LINKLOCAL);
+        assertEquals(test.label, 1);
+        assertEquals(test.precedence, 40);
+
+        // Test linklocal address
+        test = makeSortableAddress("fe80::");
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_LINKLOCAL);
+        assertEquals(test.label, 1);
+        assertEquals(test.precedence, 40);
+
+        // Test 6to4 address
+        test = makeSortableAddress("2002:c000:0204::");
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL);
+        assertEquals(test.label, 2);
+        assertEquals(test.precedence, 30);
+
+        // Test unique local address
+        test = makeSortableAddress("fc00::c000:13ab");
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL);
+        assertEquals(test.label, 13);
+        assertEquals(test.precedence, 3);
+
+        // Test teredo tunneling address
+        test = makeSortableAddress("2001::47c1");
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL);
+        assertEquals(test.label, 5);
+        assertEquals(test.precedence, 5);
+
+        // Test IPv4-compatible addresses
+        test = makeSortableAddress("::216.58.200.36");
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL);
+        assertEquals(test.label, 3);
+        assertEquals(test.precedence, 1);
+
+        // Test site-local address
+        test = makeSortableAddress("fec0::cafe:3ab2");
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_SITELOCAL);
+        assertEquals(test.label, 11);
+        assertEquals(test.precedence, 1);
+
+        // Test 6bone address
+        test = makeSortableAddress("3ffe::1234:5678");
+        assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL);
+        assertEquals(test.label, 12);
+        assertEquals(test.precedence, 1);
+    }
+}
diff --git a/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
new file mode 100644
index 0000000..b62bdbc
--- /dev/null
+++ b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.content.res.Resources
+import android.net.ConnectivityResources
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.MAX_TRANSPORT
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import org.junit.After
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+
+/**
+ * Tests for [KeepaliveUtils].
+ *
+ * Build, install and run with:
+ * atest android.net.util.KeepaliveUtilsTest
+ */
+@RunWith(JUnit4::class)
+@SmallTest
+class KeepaliveUtilsTest {
+
+    // Prepare mocked context with given resource strings.
+    private fun getMockedContextWithStringArrayRes(
+        id: Int,
+        name: String,
+        res: Array<out String?>?
+    ): Context {
+        val mockRes = mock(Resources::class.java)
+        doReturn(res).`when`(mockRes).getStringArray(eq(id))
+        doReturn(id).`when`(mockRes).getIdentifier(eq(name), any(), any())
+
+        return mock(Context::class.java).apply {
+            doReturn(mockRes).`when`(this).getResources()
+            ConnectivityResources.setResourcesContextForTest(this)
+        }
+    }
+
+    @After
+    fun tearDown() {
+        ConnectivityResources.setResourcesContextForTest(null)
+    }
+
+    @Test
+    fun testGetSupportedKeepalives() {
+        fun assertRunWithException(res: Array<out String?>?) {
+            try {
+                val mockContext = getMockedContextWithStringArrayRes(
+                        R.array.config_networkSupportedKeepaliveCount,
+                        "config_networkSupportedKeepaliveCount", res)
+                KeepaliveUtils.getSupportedKeepalives(mockContext)
+                fail("Expected KeepaliveDeviceConfigurationException")
+            } catch (expected: KeepaliveUtils.KeepaliveDeviceConfigurationException) {
+            }
+        }
+
+        // Check resource with various invalid format.
+        assertRunWithException(null)
+        assertRunWithException(arrayOf<String?>(null))
+        assertRunWithException(arrayOfNulls<String?>(10))
+        assertRunWithException(arrayOf(""))
+        assertRunWithException(arrayOf("3,ABC"))
+        assertRunWithException(arrayOf("6,3,3"))
+        assertRunWithException(arrayOf("5"))
+
+        // Check resource with invalid slots value.
+        assertRunWithException(arrayOf("3,-1"))
+
+        // Check resource with invalid transport type.
+        assertRunWithException(arrayOf("-1,3"))
+        assertRunWithException(arrayOf("10,3"))
+
+        // Check valid customization generates expected array.
+        val validRes = arrayOf("0,3", "1,0", "4,4")
+        val expectedValidRes = intArrayOf(3, 0, 0, 0, 4, 0, 0, 0)
+
+        val mockContext = getMockedContextWithStringArrayRes(
+                R.array.config_networkSupportedKeepaliveCount,
+                "config_networkSupportedKeepaliveCount", validRes)
+        val actual = KeepaliveUtils.getSupportedKeepalives(mockContext)
+        assertArrayEquals(expectedValidRes, actual)
+    }
+
+    @Test
+    fun testGetSupportedKeepalivesForNetworkCapabilities() {
+        // Mock customized supported keepalives for each transport type, and assuming:
+        //   3 for cellular,
+        //   6 for wifi,
+        //   0 for others.
+        val cust = IntArray(MAX_TRANSPORT + 1).apply {
+            this[TRANSPORT_CELLULAR] = 3
+            this[TRANSPORT_WIFI] = 6
+        }
+
+        val nc = NetworkCapabilities()
+        // Check supported keepalives with single transport type.
+        nc.transportTypes = intArrayOf(TRANSPORT_CELLULAR)
+        assertEquals(3, KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(cust, nc))
+
+        // Check supported keepalives with multiple transport types.
+        nc.transportTypes = intArrayOf(TRANSPORT_WIFI, TRANSPORT_VPN)
+        assertEquals(0, KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(cust, nc))
+
+        // Check supported keepalives with non-customized transport type.
+        nc.transportTypes = intArrayOf(TRANSPORT_ETHERNET)
+        assertEquals(0, KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(cust, nc))
+
+        // Check supported keepalives with undefined transport type.
+        nc.transportTypes = intArrayOf(MAX_TRANSPORT + 1)
+        try {
+            KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(cust, nc)
+            fail("Expected ArrayIndexOutOfBoundsException")
+        } catch (expected: ArrayIndexOutOfBoundsException) {
+        }
+    }
+}
diff --git a/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt b/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt
new file mode 100644
index 0000000..78c8fa4
--- /dev/null
+++ b/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.content.res.Resources
+import android.net.ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER
+import android.net.ConnectivityManager.MULTIPATH_PREFERENCE_PERFORMANCE
+import android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY
+import android.net.ConnectivityResources
+import android.net.ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI
+import android.net.ConnectivitySettingsManager.NETWORK_METERED_MULTIPATH_PREFERENCE
+import android.net.util.MultinetworkPolicyTracker.ActiveDataSubscriptionIdListener
+import android.provider.Settings
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyManager
+import android.test.mock.MockContentResolver
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.connectivity.resources.R
+import com.android.internal.util.test.FakeSettingsProvider
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doCallRealMethod
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+/**
+ * Tests for [MultinetworkPolicyTracker].
+ *
+ * Build, install and run with:
+ * atest android.net.util.MultinetworkPolicyTrackerTest
+ */
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class MultinetworkPolicyTrackerTest {
+    private val resources = mock(Resources::class.java).also {
+        doReturn(R.integer.config_networkAvoidBadWifi).`when`(it).getIdentifier(
+                eq("config_networkAvoidBadWifi"), eq("integer"), any())
+        doReturn(0).`when`(it).getInteger(R.integer.config_networkAvoidBadWifi)
+    }
+    private val telephonyManager = mock(TelephonyManager::class.java)
+    private val subscriptionManager = mock(SubscriptionManager::class.java).also {
+        doReturn(null).`when`(it).getActiveSubscriptionInfo(anyInt())
+    }
+    private val resolver = MockContentResolver().apply {
+        addProvider(Settings.AUTHORITY, FakeSettingsProvider()) }
+    private val context = mock(Context::class.java).also {
+        doReturn(Context.TELEPHONY_SERVICE).`when`(it)
+                .getSystemServiceName(TelephonyManager::class.java)
+        doReturn(telephonyManager).`when`(it).getSystemService(Context.TELEPHONY_SERVICE)
+        if (it.getSystemService(TelephonyManager::class.java) == null) {
+            // Test is using mockito extended
+            doCallRealMethod().`when`(it).getSystemService(TelephonyManager::class.java)
+        }
+        doReturn(subscriptionManager).`when`(it)
+                .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE)
+        doReturn(resolver).`when`(it).contentResolver
+        doReturn(resources).`when`(it).resources
+        doReturn(it).`when`(it).createConfigurationContext(any())
+        Settings.Global.putString(resolver, NETWORK_AVOID_BAD_WIFI, "1")
+        ConnectivityResources.setResourcesContextForTest(it)
+    }
+    private val tracker = MultinetworkPolicyTracker(context, null /* handler */)
+
+    private fun assertMultipathPreference(preference: Int) {
+        Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
+                preference.toString())
+        tracker.updateMeteredMultipathPreference()
+        assertEquals(preference, tracker.meteredMultipathPreference)
+    }
+
+    @After
+    fun tearDown() {
+        ConnectivityResources.setResourcesContextForTest(null)
+    }
+
+    @Test
+    fun testUpdateMeteredMultipathPreference() {
+        assertMultipathPreference(MULTIPATH_PREFERENCE_HANDOVER)
+        assertMultipathPreference(MULTIPATH_PREFERENCE_RELIABILITY)
+        assertMultipathPreference(MULTIPATH_PREFERENCE_PERFORMANCE)
+    }
+
+    @Test
+    fun testUpdateAvoidBadWifi() {
+        Settings.Global.putString(resolver, NETWORK_AVOID_BAD_WIFI, "0")
+        assertTrue(tracker.updateAvoidBadWifi())
+        assertFalse(tracker.avoidBadWifi)
+
+        doReturn(1).`when`(resources).getInteger(R.integer.config_networkAvoidBadWifi)
+        assertTrue(tracker.updateAvoidBadWifi())
+        assertTrue(tracker.avoidBadWifi)
+    }
+
+    @Test
+    fun testOnActiveDataSubscriptionIdChanged() {
+        val testSubId = 1000
+        val subscriptionInfo = SubscriptionInfo(testSubId, ""/* iccId */, 1/* iccId */,
+                "TMO"/* displayName */, "TMO"/* carrierName */, 1/* nameSource */, 1/* iconTint */,
+                "123"/* number */, 1/* roaming */, null/* icon */, "310"/* mcc */, "210"/* mnc */,
+                ""/* countryIso */, false/* isEmbedded */, null/* nativeAccessRules */,
+                "1"/* cardString */)
+        doReturn(subscriptionInfo).`when`(subscriptionManager).getActiveSubscriptionInfo(testSubId)
+
+        // Modify avoidBadWifi and meteredMultipathPreference settings value and local variables in
+        // MultinetworkPolicyTracker should be also updated after subId changed.
+        Settings.Global.putString(resolver, NETWORK_AVOID_BAD_WIFI, "0")
+        Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE,
+                MULTIPATH_PREFERENCE_PERFORMANCE.toString())
+
+        val listenerCaptor = ArgumentCaptor.forClass(
+                ActiveDataSubscriptionIdListener::class.java)
+        verify(telephonyManager, times(1))
+                .registerTelephonyCallback(any(), listenerCaptor.capture())
+        val listener = listenerCaptor.value
+        listener.onActiveDataSubscriptionIdChanged(testSubId)
+
+        // Check it get resource value with test sub id.
+        verify(subscriptionManager, times(1)).getActiveSubscriptionInfo(testSubId)
+        verify(context).createConfigurationContext(argThat { it.mcc == 310 && it.mnc == 210 })
+
+        // Check if avoidBadWifi and meteredMultipathPreference values have been updated.
+        assertFalse(tracker.avoidBadWifi)
+        assertEquals(MULTIPATH_PREFERENCE_PERFORMANCE, tracker.meteredMultipathPreference)
+    }
+}
diff --git a/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java b/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java
new file mode 100644
index 0000000..3cfecd5
--- /dev/null
+++ b/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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 com.android.internal.net;
+
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.AF_UNIX;
+import static android.system.OsConstants.EPERM;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_STREAM;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.junit.Assert.fail;
+
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.io.IoUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@androidx.test.filters.SmallTest
+public class NetworkUtilsInternalTest {
+
+    private static void expectSocketSuccess(String msg, int domain, int type) {
+        try {
+            IoUtils.closeQuietly(Os.socket(domain, type, 0));
+        } catch (ErrnoException e) {
+            fail(msg + e.getMessage());
+        }
+    }
+
+    private static void expectSocketPemissionError(String msg, int domain, int type) {
+        try {
+            IoUtils.closeQuietly(Os.socket(domain, type, 0));
+            fail(msg);
+        } catch (ErrnoException e) {
+            assertEquals(msg, e.errno, EPERM);
+        }
+    }
+
+    private static void expectHasNetworking() {
+        expectSocketSuccess("Creating a UNIX socket should not have thrown ErrnoException",
+                AF_UNIX, SOCK_STREAM);
+        expectSocketSuccess("Creating a AF_INET socket shouldn't have thrown ErrnoException",
+                AF_INET, SOCK_DGRAM);
+        expectSocketSuccess("Creating a AF_INET6 socket shouldn't have thrown ErrnoException",
+                AF_INET6, SOCK_DGRAM);
+    }
+
+    private static void expectNoNetworking() {
+        expectSocketSuccess("Creating a UNIX socket should not have thrown ErrnoException",
+                AF_UNIX, SOCK_STREAM);
+        expectSocketPemissionError(
+                "Creating a AF_INET socket should have thrown ErrnoException(EPERM)",
+                AF_INET, SOCK_DGRAM);
+        expectSocketPemissionError(
+                "Creating a AF_INET6 socket should have thrown ErrnoException(EPERM)",
+                AF_INET6, SOCK_DGRAM);
+    }
+
+    @Test
+    public void testSetAllowNetworkingForProcess() {
+        expectHasNetworking();
+        NetworkUtilsInternal.setAllowNetworkingForProcess(false);
+        expectNoNetworking();
+        NetworkUtilsInternal.setAllowNetworkingForProcess(true);
+        expectHasNetworking();
+    }
+}
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
new file mode 100644
index 0000000..cb0f071
--- /dev/null
+++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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 com.android.internal.net;
+
+import static com.android.testutils.ParcelUtils.assertParcelSane;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.IpSecAlgorithm;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Unit tests for {@link VpnProfile}. */
+@SmallTest
+@RunWith(JUnit4.class)
+public class VpnProfileTest {
+    private static final String DUMMY_PROFILE_KEY = "Test";
+
+    private static final int ENCODED_INDEX_AUTH_PARAMS_INLINE = 23;
+    private static final int ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS = 24;
+
+    @Test
+    public void testDefaults() throws Exception {
+        final VpnProfile p = new VpnProfile(DUMMY_PROFILE_KEY);
+
+        assertEquals(DUMMY_PROFILE_KEY, p.key);
+        assertEquals("", p.name);
+        assertEquals(VpnProfile.TYPE_PPTP, p.type);
+        assertEquals("", p.server);
+        assertEquals("", p.username);
+        assertEquals("", p.password);
+        assertEquals("", p.dnsServers);
+        assertEquals("", p.searchDomains);
+        assertEquals("", p.routes);
+        assertTrue(p.mppe);
+        assertEquals("", p.l2tpSecret);
+        assertEquals("", p.ipsecIdentifier);
+        assertEquals("", p.ipsecSecret);
+        assertEquals("", p.ipsecUserCert);
+        assertEquals("", p.ipsecCaCert);
+        assertEquals("", p.ipsecServerCert);
+        assertEquals(null, p.proxy);
+        assertTrue(p.getAllowedAlgorithms() != null && p.getAllowedAlgorithms().isEmpty());
+        assertFalse(p.isBypassable);
+        assertFalse(p.isMetered);
+        assertEquals(1360, p.maxMtu);
+        assertFalse(p.areAuthParamsInline);
+        assertFalse(p.isRestrictedToTestNetworks);
+    }
+
+    private VpnProfile getSampleIkev2Profile(String key) {
+        final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */);
+
+        p.name = "foo";
+        p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
+        p.server = "bar";
+        p.username = "baz";
+        p.password = "qux";
+        p.dnsServers = "8.8.8.8";
+        p.searchDomains = "";
+        p.routes = "0.0.0.0/0";
+        p.mppe = false;
+        p.l2tpSecret = "";
+        p.ipsecIdentifier = "quux";
+        p.ipsecSecret = "quuz";
+        p.ipsecUserCert = "corge";
+        p.ipsecCaCert = "grault";
+        p.ipsecServerCert = "garply";
+        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(
+                getSampleIkev2Profile(DUMMY_PROFILE_KEY), getSampleIkev2Profile(DUMMY_PROFILE_KEY));
+
+        final VpnProfile modified = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        modified.maxMtu--;
+        assertNotEquals(getSampleIkev2Profile(DUMMY_PROFILE_KEY), modified);
+    }
+
+    @Test
+    public void testParcelUnparcel() {
+        assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23);
+    }
+
+    @Test
+    public void testEncodeDecode() {
+        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
+        assertEquals(profile, decoded);
+    }
+
+    @Test
+    public void testEncodeDecodeTooManyValues() {
+        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        final byte[] tooManyValues =
+                (new String(profile.encode()) + VpnProfile.VALUE_DELIMITER + "invalid").getBytes();
+
+        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooManyValues));
+    }
+
+    private String getEncodedDecodedIkev2ProfileMissingValues(int... missingIndices) {
+        // Sort to ensure when we remove, we can do it from greatest first.
+        Arrays.sort(missingIndices);
+
+        final String encoded = new String(getSampleIkev2Profile(DUMMY_PROFILE_KEY).encode());
+        final List<String> parts =
+                new ArrayList<>(Arrays.asList(encoded.split(VpnProfile.VALUE_DELIMITER)));
+
+        // Remove from back first to ensure indexing is consistent.
+        for (int i = missingIndices.length - 1; i >= 0; i--) {
+            parts.remove(missingIndices[i]);
+        }
+
+        return String.join(VpnProfile.VALUE_DELIMITER, parts.toArray(new String[0]));
+    }
+
+    @Test
+    public void testEncodeDecodeInvalidNumberOfValues() {
+        final String tooFewValues =
+                getEncodedDecodedIkev2ProfileMissingValues(
+                        ENCODED_INDEX_AUTH_PARAMS_INLINE,
+                        ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS /* missingIndices */);
+
+        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()));
+    }
+
+    @Test
+    public void testEncodeDecodeMissingIsRestrictedToTestNetworks() {
+        final String tooFewValues =
+                getEncodedDecodedIkev2ProfileMissingValues(
+                        ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS /* missingIndices */);
+
+        // Verify decoding without isRestrictedToTestNetworks defaults to false
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes());
+        assertFalse(decoded.isRestrictedToTestNetworks);
+    }
+
+    @Test
+    public void testEncodeDecodeLoginsNotSaved() {
+        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+        profile.saveLogin = false;
+
+        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
+        assertNotEquals(profile, decoded);
+
+        // Add the username/password back, everything else must be equal.
+        decoded.username = profile.username;
+        decoded.password = profile.password;
+        assertEquals(profile, decoded);
+    }
+}
diff --git a/tests/unit/java/com/android/internal/util/BitUtilsTest.java b/tests/unit/java/com/android/internal/util/BitUtilsTest.java
new file mode 100644
index 0000000..d2fbdce
--- /dev/null
+++ b/tests/unit/java/com/android/internal/util/BitUtilsTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.internal.util;
+
+import static com.android.internal.util.BitUtils.bytesToBEInt;
+import static com.android.internal.util.BitUtils.bytesToLEInt;
+import static com.android.internal.util.BitUtils.getUint16;
+import static com.android.internal.util.BitUtils.getUint32;
+import static com.android.internal.util.BitUtils.getUint8;
+import static com.android.internal.util.BitUtils.packBits;
+import static com.android.internal.util.BitUtils.uint16;
+import static com.android.internal.util.BitUtils.uint32;
+import static com.android.internal.util.BitUtils.uint8;
+import static com.android.internal.util.BitUtils.unpackBits;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Random;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BitUtilsTest {
+
+    @Test
+    public void testUnsignedByteWideningConversions() {
+        byte b0 = 0;
+        byte b1 = 1;
+        byte bm1 = -1;
+        assertEquals(0, uint8(b0));
+        assertEquals(1, uint8(b1));
+        assertEquals(127, uint8(Byte.MAX_VALUE));
+        assertEquals(128, uint8(Byte.MIN_VALUE));
+        assertEquals(255, uint8(bm1));
+        assertEquals(255, uint8((byte)255));
+    }
+
+    @Test
+    public void testUnsignedShortWideningConversions() {
+        short s0 = 0;
+        short s1 = 1;
+        short sm1 = -1;
+        assertEquals(0, uint16(s0));
+        assertEquals(1, uint16(s1));
+        assertEquals(32767, uint16(Short.MAX_VALUE));
+        assertEquals(32768, uint16(Short.MIN_VALUE));
+        assertEquals(65535, uint16(sm1));
+        assertEquals(65535, uint16((short)65535));
+    }
+
+    @Test
+    public void testUnsignedShortComposition() {
+        byte b0 = 0;
+        byte b1 = 1;
+        byte b2 = 2;
+        byte b10 = 10;
+        byte b16 = 16;
+        byte b128 = -128;
+        byte b224 = -32;
+        byte b255 = -1;
+        assertEquals(0x0000, uint16(b0, b0));
+        assertEquals(0xffff, uint16(b255, b255));
+        assertEquals(0x0a01, uint16(b10, b1));
+        assertEquals(0x8002, uint16(b128, b2));
+        assertEquals(0x01ff, uint16(b1, b255));
+        assertEquals(0x80ff, uint16(b128, b255));
+        assertEquals(0xe010, uint16(b224, b16));
+    }
+
+    @Test
+    public void testUnsignedIntWideningConversions() {
+        assertEquals(0, uint32(0));
+        assertEquals(1, uint32(1));
+        assertEquals(2147483647L, uint32(Integer.MAX_VALUE));
+        assertEquals(2147483648L, uint32(Integer.MIN_VALUE));
+        assertEquals(4294967295L, uint32(-1));
+        assertEquals(4294967295L, uint32((int)4294967295L));
+    }
+
+    @Test
+    public void testBytesToInt() {
+        assertEquals(0x00000000, bytesToBEInt(bytes(0, 0, 0, 0)));
+        assertEquals(0xffffffff, bytesToBEInt(bytes(255, 255, 255, 255)));
+        assertEquals(0x0a000001, bytesToBEInt(bytes(10, 0, 0, 1)));
+        assertEquals(0x0a000002, bytesToBEInt(bytes(10, 0, 0, 2)));
+        assertEquals(0x0a001fff, bytesToBEInt(bytes(10, 0, 31, 255)));
+        assertEquals(0xe0000001, bytesToBEInt(bytes(224, 0, 0, 1)));
+
+        assertEquals(0x00000000, bytesToLEInt(bytes(0, 0, 0, 0)));
+        assertEquals(0x01020304, bytesToLEInt(bytes(4, 3, 2, 1)));
+        assertEquals(0xffff0000, bytesToLEInt(bytes(0, 0, 255, 255)));
+    }
+
+    @Test
+    public void testUnsignedGetters() {
+        ByteBuffer b = ByteBuffer.allocate(4);
+        b.putInt(0xffff);
+
+        assertEquals(0x0, getUint8(b, 0));
+        assertEquals(0x0, getUint8(b, 1));
+        assertEquals(0xff, getUint8(b, 2));
+        assertEquals(0xff, getUint8(b, 3));
+
+        assertEquals(0x0, getUint16(b, 0));
+        assertEquals(0xffff, getUint16(b, 2));
+
+        b.rewind();
+        b.putInt(0xffffffff);
+        assertEquals(0xffffffffL, getUint32(b, 0));
+    }
+
+    @Test
+    public void testBitsPacking() {
+        BitPackingTestCase[] testCases = {
+            new BitPackingTestCase(0, ints()),
+            new BitPackingTestCase(1, ints(0)),
+            new BitPackingTestCase(2, ints(1)),
+            new BitPackingTestCase(3, ints(0, 1)),
+            new BitPackingTestCase(4, ints(2)),
+            new BitPackingTestCase(6, ints(1, 2)),
+            new BitPackingTestCase(9, ints(0, 3)),
+            new BitPackingTestCase(~Long.MAX_VALUE, ints(63)),
+            new BitPackingTestCase(~Long.MAX_VALUE + 1, ints(0, 63)),
+            new BitPackingTestCase(~Long.MAX_VALUE + 2, ints(1, 63)),
+        };
+        for (BitPackingTestCase tc : testCases) {
+            int[] got = unpackBits(tc.packedBits);
+            assertTrue(
+                    "unpackBits("
+                            + tc.packedBits
+                            + "): expected "
+                            + Arrays.toString(tc.bits)
+                            + " but got "
+                            + Arrays.toString(got),
+                    Arrays.equals(tc.bits, got));
+        }
+        for (BitPackingTestCase tc : testCases) {
+            long got = packBits(tc.bits);
+            assertEquals(
+                    "packBits("
+                            + Arrays.toString(tc.bits)
+                            + "): expected "
+                            + tc.packedBits
+                            + " but got "
+                            + got,
+                    tc.packedBits,
+                    got);
+        }
+
+        long[] moreTestCases = {
+            0, 1, -1, 23895, -908235, Long.MAX_VALUE, Long.MIN_VALUE, new Random().nextLong(),
+        };
+        for (long l : moreTestCases) {
+            assertEquals(l, packBits(unpackBits(l)));
+        }
+    }
+
+    static byte[] bytes(int b1, int b2, int b3, int b4) {
+        return new byte[] {b(b1), b(b2), b(b3), b(b4)};
+    }
+
+    static byte b(int i) {
+        return (byte) i;
+    }
+
+    static int[] ints(int... array) {
+        return array;
+    }
+
+    static class BitPackingTestCase {
+        final int[] bits;
+        final long packedBits;
+
+        BitPackingTestCase(long packedBits, int[] bits) {
+            this.bits = bits;
+            this.packedBits = packedBits;
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/internal/util/RingBufferTest.java b/tests/unit/java/com/android/internal/util/RingBufferTest.java
new file mode 100644
index 0000000..d06095a
--- /dev/null
+++ b/tests/unit/java/com/android/internal/util/RingBufferTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.internal.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertNull;
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RingBufferTest {
+
+    @Test
+    public void testEmptyRingBuffer() {
+        RingBuffer<String> buffer = new RingBuffer<>(String.class, 100);
+
+        assertArrayEquals(new String[0], buffer.toArray());
+    }
+
+    @Test
+    public void testIncorrectConstructorArguments() {
+        try {
+            RingBuffer<String> buffer = new RingBuffer<>(String.class, -10);
+            fail("Should not be able to create a negative capacity RingBuffer");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        try {
+            RingBuffer<String> buffer = new RingBuffer<>(String.class, 0);
+            fail("Should not be able to create a 0 capacity RingBuffer");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testRingBufferWithNoWrapping() {
+        RingBuffer<String> buffer = new RingBuffer<>(String.class, 100);
+
+        buffer.append("a");
+        buffer.append("b");
+        buffer.append("c");
+        buffer.append("d");
+        buffer.append("e");
+
+        String[] expected = {"a", "b", "c", "d", "e"};
+        assertArrayEquals(expected, buffer.toArray());
+    }
+
+    @Test
+    public void testRingBufferWithCapacity1() {
+        RingBuffer<String> buffer = new RingBuffer<>(String.class, 1);
+
+        buffer.append("a");
+        assertArrayEquals(new String[]{"a"}, buffer.toArray());
+
+        buffer.append("b");
+        assertArrayEquals(new String[]{"b"}, buffer.toArray());
+
+        buffer.append("c");
+        assertArrayEquals(new String[]{"c"}, buffer.toArray());
+
+        buffer.append("d");
+        assertArrayEquals(new String[]{"d"}, buffer.toArray());
+
+        buffer.append("e");
+        assertArrayEquals(new String[]{"e"}, buffer.toArray());
+    }
+
+    @Test
+    public void testRingBufferWithWrapping() {
+        int capacity = 100;
+        RingBuffer<String> buffer = new RingBuffer<>(String.class, capacity);
+
+        buffer.append("a");
+        buffer.append("b");
+        buffer.append("c");
+        buffer.append("d");
+        buffer.append("e");
+
+        String[] expected1 = {"a", "b", "c", "d", "e"};
+        assertArrayEquals(expected1, buffer.toArray());
+
+        String[] expected2 = new String[capacity];
+        int firstIndex = 0;
+        int lastIndex = capacity - 1;
+
+        expected2[firstIndex] = "e";
+        for (int i = 1; i < capacity; i++) {
+            buffer.append("x");
+            expected2[i] = "x";
+        }
+        assertArrayEquals(expected2, buffer.toArray());
+
+        buffer.append("x");
+        expected2[firstIndex] = "x";
+        assertArrayEquals(expected2, buffer.toArray());
+
+        for (int i = 0; i < 10; i++) {
+            for (String s : expected2) {
+                buffer.append(s);
+            }
+        }
+        assertArrayEquals(expected2, buffer.toArray());
+
+        buffer.append("a");
+        expected2[lastIndex] = "a";
+        assertArrayEquals(expected2, buffer.toArray());
+    }
+
+    @Test
+    public void testGetNextSlot() {
+        int capacity = 100;
+        RingBuffer<DummyClass1> buffer = new RingBuffer<>(DummyClass1.class, capacity);
+
+        final DummyClass1[] actual = new DummyClass1[capacity];
+        final DummyClass1[] expected = new DummyClass1[capacity];
+        for (int i = 0; i < capacity; ++i) {
+            final DummyClass1 obj = buffer.getNextSlot();
+            obj.x = capacity * i;
+            actual[i] = obj;
+            expected[i] = new DummyClass1();
+            expected[i].x = capacity * i;
+        }
+        assertArrayEquals(expected, buffer.toArray());
+
+        for (int i = 0; i < capacity; ++i) {
+            if (actual[i] != buffer.getNextSlot()) {
+                fail("getNextSlot() should re-use objects if available");
+            }
+        }
+
+        RingBuffer<DummyClass2> buffer2 = new RingBuffer<>(DummyClass2.class, capacity);
+        assertNull("getNextSlot() should return null if the object can't be initiated "
+                + "(No nullary constructor)", buffer2.getNextSlot());
+
+        RingBuffer<DummyClass3> buffer3 = new RingBuffer<>(DummyClass3.class, capacity);
+        assertNull("getNextSlot() should return null if the object can't be initiated "
+                + "(Inaccessible class)", buffer3.getNextSlot());
+    }
+
+    public static final class DummyClass1 {
+        int x;
+
+        public boolean equals(Object o) {
+            if (o instanceof DummyClass1) {
+                final DummyClass1 other = (DummyClass1) o;
+                return other.x == this.x;
+            }
+            return false;
+        }
+    }
+
+    public static final class DummyClass2 {
+        public DummyClass2(int x) {}
+    }
+
+    private static final class DummyClass3 {}
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
new file mode 100644
index 0000000..3b030d6
--- /dev/null
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -0,0 +1,13394 @@
+/*
+ * 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;
+
+import static android.Manifest.permission.CHANGE_NETWORK_STATE;
+import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
+import static android.Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE;
+import static android.Manifest.permission.CREATE_USERS;
+import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.GET_INTENT_SENDER_INTENT;
+import static android.Manifest.permission.LOCAL_MAC_ADDRESS;
+import static android.Manifest.permission.NETWORK_FACTORY;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_STACK;
+import static android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
+import static android.content.Intent.ACTION_PACKAGE_ADDED;
+import static android.content.Intent.ACTION_PACKAGE_REMOVED;
+import static android.content.Intent.ACTION_PACKAGE_REPLACED;
+import static android.content.Intent.ACTION_USER_ADDED;
+import static android.content.Intent.ACTION_USER_REMOVED;
+import static android.content.Intent.ACTION_USER_UNLOCKED;
+import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
+import static android.content.pm.PackageManager.FEATURE_WIFI;
+import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT;
+import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.content.pm.PackageManager.MATCH_ANY_USER;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
+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.PROFILE_NETWORK_PREFERENCE_DEFAULT;
+import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA;
+import static android.net.ConnectivityManager.TYPE_MOBILE_MMS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
+import static android.net.ConnectivityManager.TYPE_PROXY;
+import static android.net.ConnectivityManager.TYPE_VPN;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_FALLBACK;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_BIP;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_IA;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_RCS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VSIM;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP;
+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;
+import static android.net.NetworkCapabilities.REDACT_NONE;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
+import static android.net.NetworkScore.KEEP_CONNECTED_FOR_HANDOVER;
+import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID;
+import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK;
+import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY;
+import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY;
+import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST;
+import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY;
+import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_UNINITIALIZED;
+import static android.net.RouteInfo.RTN_UNREACHABLE;
+import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_ADDED;
+import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_REMOVED;
+import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE;
+import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS;
+import static android.os.Process.INVALID_UID;
+import static android.system.OsConstants.IPPROTO_TCP;
+
+import static com.android.server.ConnectivityService.DEFAULT_NETWORK_PRIORITY_MOBILE_DATA_PREFERRED;
+import static com.android.server.ConnectivityService.DEFAULT_NETWORK_PRIORITY_OEM;
+import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
+import static com.android.testutils.ConcurrentUtils.await;
+import static com.android.testutils.ConcurrentUtils.durationOf;
+import static com.android.testutils.ExceptionUtils.ignoreExceptions;
+import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor;
+import static com.android.testutils.MiscAsserts.assertContainsAll;
+import static com.android.testutils.MiscAsserts.assertContainsExactly;
+import static com.android.testutils.MiscAsserts.assertEmpty;
+import static com.android.testutils.MiscAsserts.assertLength;
+import static com.android.testutils.MiscAsserts.assertRunsInAtMost;
+import static com.android.testutils.MiscAsserts.assertSameElements;
+import static com.android.testutils.MiscAsserts.assertThrows;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+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.AdditionalMatchers.aryEq;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.startsWith;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+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.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AlarmManager;
+import android.app.AppOpsManager;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.usage.NetworkStatsManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentProvider;
+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.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.location.LocationManager;
+import android.net.CaptivePortalData;
+import android.net.ConnectionInfo;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivityManager.PacketKeepalive;
+import android.net.ConnectivityManager.PacketKeepaliveCallback;
+import android.net.ConnectivityManager.TooManyRequestsException;
+import android.net.ConnectivityResources;
+import android.net.ConnectivitySettingsManager;
+import android.net.ConnectivityThread;
+import android.net.DataStallReportParcelable;
+import android.net.EthernetManager;
+import android.net.IConnectivityDiagnosticsCallback;
+import android.net.IDnsResolver;
+import android.net.INetd;
+import android.net.INetworkMonitor;
+import android.net.INetworkMonitorCallbacks;
+import android.net.IOnCompleteListener;
+import android.net.IQosCallback;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.net.IpSecManager;
+import android.net.IpSecManager.UdpEncapsulationSocket;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.MatchAllNetworkSpecifier;
+import android.net.NativeNetworkConfig;
+import android.net.NativeNetworkType;
+import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkFactory;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkPolicyManager;
+import android.net.NetworkPolicyManager.NetworkPolicyCallback;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.net.NetworkSpecifier;
+import android.net.NetworkStack;
+import android.net.NetworkStateSnapshot;
+import android.net.NetworkTestResultParcelable;
+import android.net.OemNetworkPreferences;
+import android.net.ProxyInfo;
+import android.net.QosCallbackException;
+import android.net.QosFilter;
+import android.net.QosSession;
+import android.net.ResolverParamsParcel;
+import android.net.RouteInfo;
+import android.net.RouteInfoParcel;
+import android.net.SocketKeepalive;
+import android.net.TransportInfo;
+import android.net.UidRange;
+import android.net.UidRangeParcel;
+import android.net.UnderlyingNetworkInfo;
+import android.net.Uri;
+import android.net.VpnManager;
+import android.net.VpnTransportInfo;
+import android.net.metrics.IpConnectivityLog;
+import android.net.networkstack.NetworkStackClientBase;
+import android.net.resolv.aidl.Nat64PrefixEventParcel;
+import android.net.resolv.aidl.PrivateDnsValidationEventParcel;
+import android.net.shared.NetworkMonitorUtils;
+import android.net.shared.PrivateDnsConfig;
+import android.net.util.MultinetworkPolicyTracker;
+import android.os.BadParcelableException;
+import android.os.BatteryStatsManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.SystemClock;
+import android.os.SystemConfigManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.security.Credentials;
+import android.system.Os;
+import android.telephony.TelephonyManager;
+import android.telephony.data.EpsBearerQosSessionAttributes;
+import android.telephony.data.NrQosSessionAttributes;
+import android.test.mock.MockContentResolver;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Range;
+import android.util.SparseArray;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.app.IBatteryStats;
+import com.android.internal.net.VpnConfig;
+import com.android.internal.net.VpnProfile;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.WakeupMessage;
+import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.net.module.util.ArrayTrackRecord;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.LocationPermissionChecker;
+import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo;
+import com.android.server.ConnectivityService.NetworkRequestInfo;
+import com.android.server.connectivity.MockableSystemProperties;
+import com.android.server.connectivity.Nat464Xlat;
+import com.android.server.connectivity.NetworkAgentInfo;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+import com.android.server.connectivity.ProxyTracker;
+import com.android.server.connectivity.QosCallbackTracker;
+import com.android.server.connectivity.Vpn;
+import com.android.server.connectivity.VpnProfileStore;
+import com.android.server.net.NetworkPinner;
+import com.android.testutils.ExceptionUtils;
+import com.android.testutils.HandlerUtils;
+import com.android.testutils.RecorderCallback.CallbackEntry;
+import com.android.testutils.TestableNetworkCallback;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.AdditionalAnswers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+import org.mockito.stubbing.Answer;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.DatagramSocket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import kotlin.reflect.KClass;
+
+/**
+ * Tests for {@link ConnectivityService}.
+ *
+ * Build, install and run with:
+ *  runtest frameworks-net -c com.android.server.ConnectivityServiceTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConnectivityServiceTest {
+    private static final String TAG = "ConnectivityServiceTest";
+
+    private static final int TIMEOUT_MS = 2_000;
+    // Broadcasts can take a long time to be delivered. The test will not wait for that long unless
+    // there is a failure, so use a long timeout.
+    private static final int BROADCAST_TIMEOUT_MS = 30_000;
+    private static final int TEST_LINGER_DELAY_MS = 400;
+    private static final int TEST_NASCENT_DELAY_MS = 300;
+    // Chosen to be less than the linger and nascent timeout. This ensures that we can distinguish
+    // between a LOST callback that arrives immediately and a LOST callback that arrives after
+    // the linger/nascent timeout. For this, our assertions should run fast enough to leave
+    // less than (mService.mLingerDelayMs - TEST_CALLBACK_TIMEOUT_MS) between the time callbacks are
+    // supposedly fired, and the time we call expectCallback.
+    private static final int TEST_CALLBACK_TIMEOUT_MS = 250;
+    // Chosen to be less than TEST_CALLBACK_TIMEOUT_MS. This ensures that requests have time to
+    // complete before callbacks are verified.
+    private static final int TEST_REQUEST_TIMEOUT_MS = 150;
+
+    private static final int UNREASONABLY_LONG_ALARM_WAIT_MS = 1000;
+
+    private static final long TIMESTAMP = 1234L;
+
+    private static final int NET_ID = 110;
+    private static final int OEM_PREF_ANY_NET_ID = -1;
+    // Set a non-zero value to verify the flow to set tcp init rwnd value.
+    private static final int TEST_TCP_INIT_RWND = 60;
+
+    // Used for testing the per-work-profile default network.
+    private static final int TEST_APP_ID = 103;
+    private static final int TEST_WORK_PROFILE_USER_ID = 2;
+    private static final int TEST_WORK_PROFILE_APP_UID =
+            UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID);
+    private static final String CLAT_PREFIX = "v4-";
+    private static final String MOBILE_IFNAME = "test_rmnet_data0";
+    private static final String CLAT_MOBILE_IFNAME = CLAT_PREFIX + MOBILE_IFNAME;
+    private static final String WIFI_IFNAME = "test_wlan0";
+    private static final String WIFI_WOL_IFNAME = "test_wlan_wol";
+    private static final String VPN_IFNAME = "tun10042";
+    private static final String TEST_PACKAGE_NAME = "com.android.test.package";
+    private static final int TEST_PACKAGE_UID = 123;
+    private static final int TEST_PACKAGE_UID2 = 321;
+    private static final String ALWAYS_ON_PACKAGE = "com.android.test.alwaysonvpn";
+
+    private static final String INTERFACE_NAME = "interface";
+
+    private static final String TEST_VENUE_URL_NA_PASSPOINT = "https://android.com/";
+    private static final String TEST_VENUE_URL_NA_OTHER = "https://example.com/";
+    private static final String TEST_TERMS_AND_CONDITIONS_URL_NA_PASSPOINT =
+            "https://android.com/terms/";
+    private static final String TEST_TERMS_AND_CONDITIONS_URL_NA_OTHER =
+            "https://example.com/terms/";
+    private static final String TEST_VENUE_URL_CAPPORT = "https://android.com/capport/";
+    private static final String TEST_USER_PORTAL_API_URL_CAPPORT =
+            "https://android.com/user/api/capport/";
+    private static final String TEST_FRIENDLY_NAME = "Network friendly name";
+    private static final String TEST_REDIRECT_URL = "http://example.com/firstPath";
+
+    private MockContext mServiceContext;
+    private HandlerThread mCsHandlerThread;
+    private HandlerThread mVMSHandlerThread;
+    private ConnectivityService.Dependencies mDeps;
+    private ConnectivityService mService;
+    private WrappedConnectivityManager mCm;
+    private TestNetworkAgentWrapper mWiFiNetworkAgent;
+    private TestNetworkAgentWrapper mCellNetworkAgent;
+    private TestNetworkAgentWrapper mEthernetNetworkAgent;
+    private MockVpn mMockVpn;
+    private Context mContext;
+    private NetworkPolicyCallback mPolicyCallback;
+    private WrappedMultinetworkPolicyTracker mPolicyTracker;
+    private HandlerThread mAlarmManagerThread;
+    private TestNetIdManager mNetIdManager;
+    private QosCallbackMockHelper mQosCallbackMockHelper;
+    private QosCallbackTracker mQosCallbackTracker;
+    private VpnManagerService mVpnManagerService;
+    private TestNetworkCallback mDefaultNetworkCallback;
+    private TestNetworkCallback mSystemDefaultNetworkCallback;
+    private TestNetworkCallback mProfileDefaultNetworkCallback;
+    private TestNetworkCallback mTestPackageDefaultNetworkCallback;
+
+    // State variables required to emulate NetworkPolicyManagerService behaviour.
+    private int mBlockedReasons = BLOCKED_REASON_NONE;
+
+    @Mock DeviceIdleInternal mDeviceIdleInternal;
+    @Mock INetworkManagementService mNetworkManagementService;
+    @Mock NetworkStatsManager mStatsManager;
+    @Mock IDnsResolver mMockDnsResolver;
+    @Mock INetd mMockNetd;
+    @Mock NetworkStackClientBase mNetworkStack;
+    @Mock PackageManager mPackageManager;
+    @Mock UserManager mUserManager;
+    @Mock NotificationManager mNotificationManager;
+    @Mock AlarmManager mAlarmManager;
+    @Mock IConnectivityDiagnosticsCallback mConnectivityDiagnosticsCallback;
+    @Mock IBinder mIBinder;
+    @Mock LocationManager mLocationManager;
+    @Mock AppOpsManager mAppOpsManager;
+    @Mock TelephonyManager mTelephonyManager;
+    @Mock MockableSystemProperties mSystemProperties;
+    @Mock EthernetManager mEthernetManager;
+    @Mock NetworkPolicyManager mNetworkPolicyManager;
+    @Mock VpnProfileStore mVpnProfileStore;
+    @Mock SystemConfigManager mSystemConfigManager;
+    @Mock Resources mResources;
+    @Mock ProxyTracker mProxyTracker;
+
+    // BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
+    // underlying binder calls.
+    final BatteryStatsManager mBatteryStatsManager =
+            new BatteryStatsManager(mock(IBatteryStats.class));
+
+    private ArgumentCaptor<ResolverParamsParcel> mResolverParamsParcelCaptor =
+            ArgumentCaptor.forClass(ResolverParamsParcel.class);
+
+    // This class exists to test bindProcessToNetwork and getBoundNetworkForProcess. These methods
+    // do not go through ConnectivityService but talk to netd directly, so they don't automatically
+    // reflect the state of our test ConnectivityService.
+    private class WrappedConnectivityManager extends ConnectivityManager {
+        private Network mFakeBoundNetwork;
+
+        public synchronized boolean bindProcessToNetwork(Network network) {
+            mFakeBoundNetwork = network;
+            return true;
+        }
+
+        public synchronized Network getBoundNetworkForProcess() {
+            return mFakeBoundNetwork;
+        }
+
+        public WrappedConnectivityManager(Context context, ConnectivityService service) {
+            super(context, service);
+        }
+    }
+
+    private class MockContext extends BroadcastInterceptingContext {
+        private final MockContentResolver mContentResolver;
+
+        @Spy private Resources mInternalResources;
+        private final LinkedBlockingQueue<Intent> mStartedActivities = new LinkedBlockingQueue<>();
+
+        // Map of permission name -> PermissionManager.Permission_{GRANTED|DENIED} constant
+        private final HashMap<String, Integer> mMockedPermissions = new HashMap<>();
+
+        MockContext(Context base, ContentProvider settingsProvider) {
+            super(base);
+
+            mInternalResources = spy(base.getResources());
+            when(mInternalResources.getStringArray(com.android.internal.R.array.networkAttributes))
+                    .thenReturn(new String[] {
+                            "wifi,1,1,1,-1,true",
+                            "mobile,0,0,0,-1,true",
+                            "mobile_mms,2,0,2,60000,true",
+                            "mobile_supl,3,0,2,60000,true",
+                    });
+
+            mContentResolver = new MockContentResolver();
+            mContentResolver.addProvider(Settings.AUTHORITY, settingsProvider);
+        }
+
+        @Override
+        public void startActivityAsUser(Intent intent, UserHandle handle) {
+            mStartedActivities.offer(intent);
+        }
+
+        public Intent expectStartActivityIntent(int timeoutMs) {
+            Intent intent = null;
+            try {
+                intent = mStartedActivities.poll(timeoutMs, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {}
+            assertNotNull("Did not receive sign-in intent after " + timeoutMs + "ms", intent);
+            return intent;
+        }
+
+        public void expectNoStartActivityIntent(int timeoutMs) {
+            try {
+                assertNull("Received unexpected Intent to start activity",
+                        mStartedActivities.poll(timeoutMs, TimeUnit.MILLISECONDS));
+            } catch (InterruptedException e) {}
+        }
+
+        @Override
+        public ComponentName startService(Intent service) {
+            final String action = service.getAction();
+            if (!VpnConfig.SERVICE_INTERFACE.equals(action)) {
+                fail("Attempt to start unknown service, action=" + action);
+            }
+            return new ComponentName(service.getPackage(), "com.android.test.Service");
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            if (Context.CONNECTIVITY_SERVICE.equals(name)) return mCm;
+            if (Context.NOTIFICATION_SERVICE.equals(name)) return mNotificationManager;
+            if (Context.USER_SERVICE.equals(name)) return mUserManager;
+            if (Context.ALARM_SERVICE.equals(name)) return mAlarmManager;
+            if (Context.LOCATION_SERVICE.equals(name)) return mLocationManager;
+            if (Context.APP_OPS_SERVICE.equals(name)) return mAppOpsManager;
+            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.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;
+            return super.getSystemService(name);
+        }
+
+        final HashMap<UserHandle, UserManager> mUserManagers = new HashMap<>();
+        @Override
+        public Context createContextAsUser(UserHandle user, int flags) {
+            final Context asUser = mock(Context.class, AdditionalAnswers.delegatesTo(this));
+            doReturn(user).when(asUser).getUser();
+            doAnswer((inv) -> {
+                final UserManager um = mUserManagers.computeIfAbsent(user,
+                        u -> mock(UserManager.class, AdditionalAnswers.delegatesTo(mUserManager)));
+                return um;
+            }).when(asUser).getSystemService(Context.USER_SERVICE);
+            return asUser;
+        }
+
+        public void setWorkProfile(@NonNull final UserHandle userHandle, boolean value) {
+            // This relies on all contexts for a given user returning the same UM mock
+            final UserManager umMock = createContextAsUser(userHandle, 0 /* flags */)
+                    .getSystemService(UserManager.class);
+            doReturn(value).when(umMock).isManagedProfile();
+            doReturn(value).when(mUserManager).isManagedProfile(eq(userHandle.getIdentifier()));
+        }
+
+        @Override
+        public ContentResolver getContentResolver() {
+            return mContentResolver;
+        }
+
+        @Override
+        public Resources getResources() {
+            return mInternalResources;
+        }
+
+        @Override
+        public PackageManager getPackageManager() {
+            return mPackageManager;
+        }
+
+        private int checkMockedPermission(String permission, Supplier<Integer> ifAbsent) {
+            final Integer granted = mMockedPermissions.get(permission);
+            return granted != null ? granted : ifAbsent.get();
+        }
+
+        @Override
+        public int checkPermission(String permission, int pid, int uid) {
+            return checkMockedPermission(
+                    permission, () -> super.checkPermission(permission, pid, uid));
+        }
+
+        @Override
+        public int checkCallingOrSelfPermission(String permission) {
+            return checkMockedPermission(
+                    permission, () -> super.checkCallingOrSelfPermission(permission));
+        }
+
+        @Override
+        public void enforceCallingOrSelfPermission(String permission, String message) {
+            final Integer granted = mMockedPermissions.get(permission);
+            if (granted == null) {
+                super.enforceCallingOrSelfPermission(permission, message);
+                return;
+            }
+
+            if (!granted.equals(PERMISSION_GRANTED)) {
+                throw new SecurityException("[Test] permission denied: " + permission);
+            }
+        }
+
+        /**
+         * Mock checks for the specified permission, and have them behave as per {@code granted}.
+         *
+         * <p>Passing null reverts to default behavior, which does a real permission check on the
+         * test package.
+         * @param granted One of {@link PackageManager#PERMISSION_GRANTED} or
+         *                {@link PackageManager#PERMISSION_DENIED}.
+         */
+        public void setPermission(String permission, Integer granted) {
+            mMockedPermissions.put(permission, granted);
+        }
+
+        @Override
+        public Intent registerReceiverForAllUsers(@Nullable BroadcastReceiver receiver,
+                @NonNull IntentFilter filter, @Nullable String broadcastPermission,
+                @Nullable Handler scheduler) {
+            // TODO: ensure MultinetworkPolicyTracker's BroadcastReceiver is tested; just returning
+            // null should not pass the test
+            return null;
+        }
+    }
+
+    private void waitForIdle() {
+        HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
+        waitForIdle(mCellNetworkAgent, TIMEOUT_MS);
+        waitForIdle(mWiFiNetworkAgent, TIMEOUT_MS);
+        waitForIdle(mEthernetNetworkAgent, TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
+        HandlerUtils.waitForIdle(ConnectivityThread.get(), TIMEOUT_MS);
+    }
+
+    private void waitForIdle(TestNetworkAgentWrapper agent, long timeoutMs) {
+        if (agent == null) {
+            return;
+        }
+        agent.waitForIdle(timeoutMs);
+    }
+
+    @Test
+    public void testWaitForIdle() throws Exception {
+        final int attempts = 50;  // Causes the test to take about 200ms on bullhead-eng.
+
+        // Tests that waitForIdle returns immediately if the service is already idle.
+        for (int i = 0; i < attempts; i++) {
+            waitForIdle();
+        }
+
+        // Bring up a network that we can use to send messages to ConnectivityService.
+        ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        b.expectBroadcast();
+        Network n = mWiFiNetworkAgent.getNetwork();
+        assertNotNull(n);
+
+        // Tests that calling waitForIdle waits for messages to be processed.
+        for (int i = 0; i < attempts; i++) {
+            mWiFiNetworkAgent.setSignalStrength(i);
+            waitForIdle();
+            assertEquals(i, mCm.getNetworkCapabilities(n).getSignalStrength());
+        }
+    }
+
+    // This test has an inherent race condition in it, and cannot be enabled for continuous testing
+    // or presubmit tests. It is kept for manual runs and documentation purposes.
+    @Ignore
+    public void verifyThatNotWaitingForIdleCausesRaceConditions() throws Exception {
+        // Bring up a network that we can use to send messages to ConnectivityService.
+        ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        b.expectBroadcast();
+        Network n = mWiFiNetworkAgent.getNetwork();
+        assertNotNull(n);
+
+        // Ensure that not calling waitForIdle causes a race condition.
+        final int attempts = 50;  // Causes the test to take about 200ms on bullhead-eng.
+        for (int i = 0; i < attempts; i++) {
+            mWiFiNetworkAgent.setSignalStrength(i);
+            if (i != mCm.getNetworkCapabilities(n).getSignalStrength()) {
+                // We hit a race condition, as expected. Pass the test.
+                return;
+            }
+        }
+
+        // No race? There is a bug in this test.
+        fail("expected race condition at least once in " + attempts + " attempts");
+    }
+
+    private class TestNetworkAgentWrapper extends NetworkAgentWrapper {
+        private static final int VALIDATION_RESULT_INVALID = 0;
+
+        private static final long DATA_STALL_TIMESTAMP = 10L;
+        private static final int DATA_STALL_DETECTION_METHOD = 1;
+
+        private INetworkMonitor mNetworkMonitor;
+        private INetworkMonitorCallbacks mNmCallbacks;
+        private int mNmValidationResult = VALIDATION_RESULT_INVALID;
+        private int mProbesCompleted;
+        private int mProbesSucceeded;
+        private String mNmValidationRedirectUrl = null;
+        private boolean mNmProvNotificationRequested = false;
+        private Runnable mCreatedCallback;
+        private Runnable mUnwantedCallback;
+        private Runnable mDisconnectedCallback;
+
+        private final ConditionVariable mNetworkStatusReceived = new ConditionVariable();
+        // Contains the redirectUrl from networkStatus(). Before reading, wait for
+        // mNetworkStatusReceived.
+        private String mRedirectUrl;
+
+        TestNetworkAgentWrapper(int transport) throws Exception {
+            this(transport, new LinkProperties(), null);
+        }
+
+        TestNetworkAgentWrapper(int transport, LinkProperties linkProperties)
+                throws Exception {
+            this(transport, linkProperties, null);
+        }
+
+        private TestNetworkAgentWrapper(int transport, LinkProperties linkProperties,
+                NetworkCapabilities ncTemplate) throws Exception {
+            super(transport, linkProperties, ncTemplate, mServiceContext);
+
+            // Waits for the NetworkAgent to be registered, which includes the creation of the
+            // NetworkMonitor.
+            waitForIdle(TIMEOUT_MS);
+            HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
+            HandlerUtils.waitForIdle(ConnectivityThread.get(), TIMEOUT_MS);
+        }
+
+        @Override
+        protected InstrumentedNetworkAgent makeNetworkAgent(LinkProperties linkProperties,
+                NetworkAgentConfig nac) throws Exception {
+            mNetworkMonitor = mock(INetworkMonitor.class);
+
+            final Answer validateAnswer = inv -> {
+                new Thread(ignoreExceptions(this::onValidationRequested)).start();
+                return null;
+            };
+
+            doAnswer(validateAnswer).when(mNetworkMonitor).notifyNetworkConnected(any(), any());
+            doAnswer(validateAnswer).when(mNetworkMonitor).forceReevaluation(anyInt());
+
+            final ArgumentCaptor<Network> nmNetworkCaptor = ArgumentCaptor.forClass(Network.class);
+            final ArgumentCaptor<INetworkMonitorCallbacks> nmCbCaptor =
+                    ArgumentCaptor.forClass(INetworkMonitorCallbacks.class);
+            doNothing().when(mNetworkStack).makeNetworkMonitor(
+                    nmNetworkCaptor.capture(),
+                    any() /* name */,
+                    nmCbCaptor.capture());
+
+            final InstrumentedNetworkAgent na =
+                    new InstrumentedNetworkAgent(this, linkProperties, nac) {
+                @Override
+                public void networkStatus(int status, String redirectUrl) {
+                    mRedirectUrl = redirectUrl;
+                    mNetworkStatusReceived.open();
+                }
+
+                @Override
+                public void onNetworkCreated() {
+                    super.onNetworkCreated();
+                    if (mCreatedCallback != null) mCreatedCallback.run();
+                }
+
+                @Override
+                public void onNetworkUnwanted() {
+                    super.onNetworkUnwanted();
+                    if (mUnwantedCallback != null) mUnwantedCallback.run();
+                }
+
+                @Override
+                public void onNetworkDestroyed() {
+                    super.onNetworkDestroyed();
+                    if (mDisconnectedCallback != null) mDisconnectedCallback.run();
+                }
+            };
+
+            assertEquals(na.getNetwork().netId, nmNetworkCaptor.getValue().netId);
+            mNmCallbacks = nmCbCaptor.getValue();
+
+            mNmCallbacks.onNetworkMonitorCreated(mNetworkMonitor);
+
+            return na;
+        }
+
+        private void onValidationRequested() throws Exception {
+            if (mNmProvNotificationRequested
+                    && ((mNmValidationResult & NETWORK_VALIDATION_RESULT_VALID) != 0)) {
+                mNmCallbacks.hideProvisioningNotification();
+                mNmProvNotificationRequested = false;
+            }
+
+            mNmCallbacks.notifyProbeStatusChanged(mProbesCompleted, mProbesSucceeded);
+            final NetworkTestResultParcelable p = new NetworkTestResultParcelable();
+            p.result = mNmValidationResult;
+            p.probesAttempted = mProbesCompleted;
+            p.probesSucceeded = mProbesSucceeded;
+            p.redirectUrl = mNmValidationRedirectUrl;
+            p.timestampMillis = TIMESTAMP;
+            mNmCallbacks.notifyNetworkTestedWithExtras(p);
+
+            if (mNmValidationRedirectUrl != null) {
+                mNmCallbacks.showProvisioningNotification(
+                        "test_provisioning_notif_action", TEST_PACKAGE_NAME);
+                mNmProvNotificationRequested = true;
+            }
+        }
+
+        /**
+         * Connect without adding any internet capability.
+         */
+        public void connectWithoutInternet() {
+            super.connect();
+        }
+
+        /**
+         * Transition this NetworkAgent to CONNECTED state with NET_CAPABILITY_INTERNET.
+         * @param validated Indicate if network should pretend to be validated.
+         */
+        public void connect(boolean validated) {
+            connect(validated, true, false /* isStrictMode */);
+        }
+
+        /**
+         * Transition this NetworkAgent to CONNECTED state.
+         * @param validated Indicate if network should pretend to be validated.
+         * @param hasInternet Indicate if network should pretend to have NET_CAPABILITY_INTERNET.
+         */
+        public void connect(boolean validated, boolean hasInternet, boolean isStrictMode) {
+            assertFalse(getNetworkCapabilities().hasCapability(NET_CAPABILITY_INTERNET));
+
+            ConnectivityManager.NetworkCallback callback = null;
+            final ConditionVariable validatedCv = new ConditionVariable();
+            if (validated) {
+                setNetworkValid(isStrictMode);
+                NetworkRequest request = new NetworkRequest.Builder()
+                        .addTransportType(getNetworkCapabilities().getTransportTypes()[0])
+                        .clearCapabilities()
+                        .build();
+                callback = new ConnectivityManager.NetworkCallback() {
+                    public void onCapabilitiesChanged(Network network,
+                            NetworkCapabilities networkCapabilities) {
+                        if (network.equals(getNetwork()) &&
+                                networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)) {
+                            validatedCv.open();
+                        }
+                    }
+                };
+                mCm.registerNetworkCallback(request, callback);
+            }
+            if (hasInternet) {
+                addCapability(NET_CAPABILITY_INTERNET);
+            }
+
+            connectWithoutInternet();
+
+            if (validated) {
+                // Wait for network to validate.
+                waitFor(validatedCv);
+                setNetworkInvalid(isStrictMode);
+            }
+
+            if (callback != null) mCm.unregisterNetworkCallback(callback);
+        }
+
+        public void connectWithCaptivePortal(String redirectUrl, boolean isStrictMode) {
+            setNetworkPortal(redirectUrl, isStrictMode);
+            connect(false, true /* hasInternet */, isStrictMode);
+        }
+
+        public void connectWithPartialConnectivity() {
+            setNetworkPartial();
+            connect(false);
+        }
+
+        public void connectWithPartialValidConnectivity(boolean isStrictMode) {
+            setNetworkPartialValid(isStrictMode);
+            connect(false, true /* hasInternet */, isStrictMode);
+        }
+
+        void setNetworkValid(boolean isStrictMode) {
+            mNmValidationResult = NETWORK_VALIDATION_RESULT_VALID;
+            mNmValidationRedirectUrl = null;
+            int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS;
+            if (isStrictMode) {
+                probesSucceeded |= NETWORK_VALIDATION_PROBE_PRIVDNS;
+            }
+            // The probesCompleted equals to probesSucceeded for the case of valid network, so put
+            // the same value into two different parameter of the method.
+            setProbesStatus(probesSucceeded, probesSucceeded);
+        }
+
+        void setNetworkInvalid(boolean isStrictMode) {
+            mNmValidationResult = VALIDATION_RESULT_INVALID;
+            mNmValidationRedirectUrl = null;
+            int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS
+                    | NETWORK_VALIDATION_PROBE_HTTP;
+            int probesSucceeded = 0;
+            // If the isStrictMode is true, it means the network is invalid when NetworkMonitor
+            // tried to validate the private DNS but failed.
+            if (isStrictMode) {
+                probesCompleted &= ~NETWORK_VALIDATION_PROBE_HTTP;
+                probesSucceeded = probesCompleted;
+                probesCompleted |= NETWORK_VALIDATION_PROBE_PRIVDNS;
+            }
+            setProbesStatus(probesCompleted, probesSucceeded);
+        }
+
+        void setNetworkPortal(String redirectUrl, boolean isStrictMode) {
+            setNetworkInvalid(isStrictMode);
+            mNmValidationRedirectUrl = redirectUrl;
+            // Suppose the portal is found when NetworkMonitor probes NETWORK_VALIDATION_PROBE_HTTP
+            // in the beginning, so the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet.
+            int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTP;
+            int probesSucceeded = VALIDATION_RESULT_INVALID;
+            if (isStrictMode) {
+                probesCompleted |= NETWORK_VALIDATION_PROBE_PRIVDNS;
+            }
+            setProbesStatus(probesCompleted, probesSucceeded);
+        }
+
+        void setNetworkPartial() {
+            mNmValidationResult = NETWORK_VALIDATION_RESULT_PARTIAL;
+            mNmValidationRedirectUrl = null;
+            int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS
+                    | NETWORK_VALIDATION_PROBE_FALLBACK;
+            int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK;
+            setProbesStatus(probesCompleted, probesSucceeded);
+        }
+
+        void setNetworkPartialValid(boolean isStrictMode) {
+            setNetworkPartial();
+            mNmValidationResult |= NETWORK_VALIDATION_RESULT_VALID;
+            int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS
+                    | NETWORK_VALIDATION_PROBE_HTTP;
+            int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTP;
+            // Suppose the partial network cannot pass the private DNS validation as well, so only
+            // add NETWORK_VALIDATION_PROBE_DNS in probesCompleted but not probesSucceeded.
+            if (isStrictMode) {
+                probesCompleted |= NETWORK_VALIDATION_PROBE_PRIVDNS;
+            }
+            setProbesStatus(probesCompleted, probesSucceeded);
+        }
+
+        void setProbesStatus(int probesCompleted, int probesSucceeded) {
+            mProbesCompleted = probesCompleted;
+            mProbesSucceeded = probesSucceeded;
+        }
+
+        void notifyCapportApiDataChanged(CaptivePortalData data) {
+            try {
+                mNmCallbacks.notifyCaptivePortalDataChanged(data);
+            } catch (RemoteException e) {
+                throw new AssertionError("This cannot happen", e);
+            }
+        }
+
+        public String waitForRedirectUrl() {
+            assertTrue(mNetworkStatusReceived.block(TIMEOUT_MS));
+            return mRedirectUrl;
+        }
+
+        public void expectDisconnected() {
+            expectDisconnected(TIMEOUT_MS);
+        }
+
+        public void expectPreventReconnectReceived() {
+            expectPreventReconnectReceived(TIMEOUT_MS);
+        }
+
+        void notifyDataStallSuspected() throws Exception {
+            final DataStallReportParcelable p = new DataStallReportParcelable();
+            p.detectionMethod = DATA_STALL_DETECTION_METHOD;
+            p.timestampMillis = DATA_STALL_TIMESTAMP;
+            mNmCallbacks.notifyDataStallSuspected(p);
+        }
+
+        public void setCreatedCallback(Runnable r) {
+            mCreatedCallback = r;
+        }
+
+        public void setUnwantedCallback(Runnable r) {
+            mUnwantedCallback = r;
+        }
+
+        public void setDisconnectedCallback(Runnable r) {
+            mDisconnectedCallback = r;
+        }
+    }
+
+    /**
+     * A NetworkFactory that allows to wait until any in-flight NetworkRequest add or remove
+     * operations have been processed and test for them.
+     */
+    private static class MockNetworkFactory extends NetworkFactory {
+        private final AtomicBoolean mNetworkStarted = new AtomicBoolean(false);
+
+        static class RequestEntry {
+            @NonNull
+            public final NetworkRequest request;
+
+            RequestEntry(@NonNull final NetworkRequest request) {
+                this.request = request;
+            }
+
+            static final class Add extends RequestEntry {
+                Add(@NonNull final NetworkRequest request) {
+                    super(request);
+                }
+            }
+
+            static final class Remove extends RequestEntry {
+                Remove(@NonNull final NetworkRequest request) {
+                    super(request);
+                }
+            }
+
+            @Override
+            public String toString() {
+                return "RequestEntry [ " + getClass().getName() + " : " + request + " ]";
+            }
+        }
+
+        // History of received requests adds and removes.
+        private final ArrayTrackRecord<RequestEntry>.ReadHead mRequestHistory =
+                new ArrayTrackRecord<RequestEntry>().newReadHead();
+
+        private static <T> T failIfNull(@Nullable final T obj, @Nullable final String message) {
+            if (null == obj) fail(null != message ? message : "Must not be null");
+            return obj;
+        }
+
+        public RequestEntry.Add expectRequestAdd() {
+            return failIfNull((RequestEntry.Add) mRequestHistory.poll(TIMEOUT_MS,
+                    it -> it instanceof RequestEntry.Add), "Expected request add");
+        }
+
+        public void expectRequestAdds(final int count) {
+            for (int i = count; i > 0; --i) {
+                expectRequestAdd();
+            }
+        }
+
+        public RequestEntry.Remove expectRequestRemove() {
+            return failIfNull((RequestEntry.Remove) mRequestHistory.poll(TIMEOUT_MS,
+                    it -> it instanceof RequestEntry.Remove), "Expected request remove");
+        }
+
+        public void expectRequestRemoves(final int count) {
+            for (int i = count; i > 0; --i) {
+                expectRequestRemove();
+            }
+        }
+
+        // Used to collect the networks requests managed by this factory. This is a duplicate of
+        // the internal information stored in the NetworkFactory (which is private).
+        private SparseArray<NetworkRequest> mNetworkRequests = new SparseArray<>();
+        private final HandlerThread mHandlerSendingRequests;
+
+        public MockNetworkFactory(Looper looper, Context context, String logTag,
+                NetworkCapabilities filter, HandlerThread threadSendingRequests) {
+            super(looper, context, logTag, filter);
+            mHandlerSendingRequests = threadSendingRequests;
+        }
+
+        public int getMyRequestCount() {
+            return getRequestCount();
+        }
+
+        protected void startNetwork() {
+            mNetworkStarted.set(true);
+        }
+
+        protected void stopNetwork() {
+            mNetworkStarted.set(false);
+        }
+
+        public boolean getMyStartRequested() {
+            return mNetworkStarted.get();
+        }
+
+
+        @Override
+        protected void needNetworkFor(NetworkRequest request) {
+            mNetworkRequests.put(request.requestId, request);
+            super.needNetworkFor(request);
+            mRequestHistory.add(new RequestEntry.Add(request));
+        }
+
+        @Override
+        protected void releaseNetworkFor(NetworkRequest request) {
+            mNetworkRequests.remove(request.requestId);
+            super.releaseNetworkFor(request);
+            mRequestHistory.add(new RequestEntry.Remove(request));
+        }
+
+        public void assertRequestCountEquals(final int count) {
+            assertEquals(count, getMyRequestCount());
+        }
+
+        @Override
+        public void terminate() {
+            super.terminate();
+            // Make sure there are no remaining requests unaccounted for.
+            HandlerUtils.waitForIdle(mHandlerSendingRequests, TIMEOUT_MS);
+            assertNull(mRequestHistory.poll(0, r -> true));
+        }
+
+        // Trigger releasing the request as unfulfillable
+        public void triggerUnfulfillable(NetworkRequest r) {
+            super.releaseRequestAsUnfulfillableByAnyFactory(r);
+        }
+
+        public void assertNoRequestChanged() {
+            assertNull(mRequestHistory.poll(0, r -> true));
+        }
+    }
+
+    private Set<UidRange> uidRangesForUids(int... uids) {
+        final ArraySet<UidRange> ranges = new ArraySet<>();
+        for (final int uid : uids) {
+            ranges.add(new UidRange(uid, uid));
+        }
+        return ranges;
+    }
+
+    private Set<UidRange> uidRangesForUids(Collection<Integer> uids) {
+        return uidRangesForUids(CollectionUtils.toIntArray(uids));
+    }
+
+    private static Looper startHandlerThreadAndReturnLooper() {
+        final HandlerThread handlerThread = new HandlerThread("MockVpnThread");
+        handlerThread.start();
+        return handlerThread.getLooper();
+    }
+
+    private class MockVpn extends Vpn implements TestableNetworkCallback.HasNetwork {
+        // Careful ! This is different from mNetworkAgent, because MockNetworkAgent does
+        // not inherit from NetworkAgent.
+        private TestNetworkAgentWrapper mMockNetworkAgent;
+        private boolean mAgentRegistered = false;
+
+        private int mVpnType = VpnManager.TYPE_VPN_SERVICE;
+        private UnderlyingNetworkInfo mUnderlyingNetworkInfo;
+
+        // These ConditionVariables allow tests to wait for LegacyVpnRunner to be stopped/started.
+        // TODO: this scheme is ad-hoc and error-prone because it does not fail if, for example, the
+        // test expects two starts in a row, or even if the production code calls start twice in a
+        // row. find a better solution. Simply putting a method to create a LegacyVpnRunner into
+        // Vpn.Dependencies doesn't work because LegacyVpnRunner is not a static class and has
+        // extensive access into the internals of Vpn.
+        private ConditionVariable mStartLegacyVpnCv = new ConditionVariable();
+        private ConditionVariable mStopVpnRunnerCv = new ConditionVariable();
+
+        public MockVpn(int userId) {
+            super(startHandlerThreadAndReturnLooper(), mServiceContext,
+                    new Dependencies() {
+                        @Override
+                        public boolean isCallerSystem() {
+                            return true;
+                        }
+
+                        @Override
+                        public DeviceIdleInternal getDeviceIdleInternal() {
+                            return mDeviceIdleInternal;
+                        }
+                    },
+                    mNetworkManagementService, mMockNetd, userId, mVpnProfileStore,
+                    new SystemServices(mServiceContext) {
+                        @Override
+                        public String settingsSecureGetStringForUser(String key, int userId) {
+                            switch (key) {
+                                // Settings keys not marked as @Readable are not readable from
+                                // non-privileged apps, unless marked as testOnly=true
+                                // (atest refuses to install testOnly=true apps), even if mocked
+                                // in the content provider, because
+                                // Settings.Secure.NameValueCache#getStringForUser checks the key
+                                // before querying the mock settings provider.
+                                case Settings.Secure.ALWAYS_ON_VPN_APP:
+                                    return null;
+                                default:
+                                    return super.settingsSecureGetStringForUser(key, userId);
+                            }
+                        }
+                    }, new Ikev2SessionCreator());
+        }
+
+        public void setUids(Set<UidRange> uids) {
+            mNetworkCapabilities.setUids(UidRange.toIntRanges(uids));
+            if (mAgentRegistered) {
+                mMockNetworkAgent.setNetworkCapabilities(mNetworkCapabilities, true);
+            }
+        }
+
+        public void setVpnType(int vpnType) {
+            mVpnType = vpnType;
+        }
+
+        @Override
+        public Network getNetwork() {
+            return (mMockNetworkAgent == null) ? null : mMockNetworkAgent.getNetwork();
+        }
+
+        @Override
+        public int getActiveVpnType() {
+            return mVpnType;
+        }
+
+        private LinkProperties makeLinkProperties() {
+            final LinkProperties lp = new LinkProperties();
+            lp.setInterfaceName(VPN_IFNAME);
+            return lp;
+        }
+
+        private void registerAgent(boolean isAlwaysMetered, Set<UidRange> uids, LinkProperties lp)
+                throws Exception {
+            if (mAgentRegistered) throw new IllegalStateException("already registered");
+            updateState(NetworkInfo.DetailedState.CONNECTING, "registerAgent");
+            mConfig = new VpnConfig();
+            mConfig.session = "MySession12345";
+            setUids(uids);
+            if (!isAlwaysMetered) mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED);
+            mInterface = VPN_IFNAME;
+            mNetworkCapabilities.setTransportInfo(new VpnTransportInfo(getActiveVpnType(),
+                    mConfig.session));
+            mMockNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_VPN, lp,
+                    mNetworkCapabilities);
+            mMockNetworkAgent.waitForIdle(TIMEOUT_MS);
+
+            verify(mMockNetd, times(1)).networkAddUidRanges(eq(mMockVpn.getNetwork().getNetId()),
+                    eq(toUidRangeStableParcels(uids)));
+            verify(mMockNetd, never())
+                    .networkRemoveUidRanges(eq(mMockVpn.getNetwork().getNetId()), any());
+            mAgentRegistered = true;
+            verify(mMockNetd).networkCreate(nativeNetworkConfigVpn(getNetwork().netId,
+                    !mMockNetworkAgent.isBypassableVpn(), mVpnType));
+            updateState(NetworkInfo.DetailedState.CONNECTED, "registerAgent");
+            mNetworkCapabilities.set(mMockNetworkAgent.getNetworkCapabilities());
+            mNetworkAgent = mMockNetworkAgent.getNetworkAgent();
+        }
+
+        private void registerAgent(Set<UidRange> uids) throws Exception {
+            registerAgent(false /* isAlwaysMetered */, uids, makeLinkProperties());
+        }
+
+        private void connect(boolean validated, boolean hasInternet, boolean isStrictMode) {
+            mMockNetworkAgent.connect(validated, hasInternet, isStrictMode);
+        }
+
+        private void connect(boolean validated) {
+            mMockNetworkAgent.connect(validated);
+        }
+
+        private TestNetworkAgentWrapper getAgent() {
+            return mMockNetworkAgent;
+        }
+
+        private void setOwnerAndAdminUid(int uid) throws Exception {
+            mNetworkCapabilities.setOwnerUid(uid);
+            mNetworkCapabilities.setAdministratorUids(new int[]{uid});
+        }
+
+        public void establish(LinkProperties lp, int uid, Set<UidRange> ranges, boolean validated,
+                boolean hasInternet, boolean isStrictMode) throws Exception {
+            setOwnerAndAdminUid(uid);
+            registerAgent(false, ranges, lp);
+            connect(validated, hasInternet, isStrictMode);
+            waitForIdle();
+        }
+
+        public void establish(LinkProperties lp, int uid, Set<UidRange> ranges) throws Exception {
+            establish(lp, uid, ranges, true, true, false);
+        }
+
+        public void establishForMyUid(LinkProperties lp) throws Exception {
+            final int uid = Process.myUid();
+            establish(lp, uid, uidRangesForUids(uid), true, true, false);
+        }
+
+        public void establishForMyUid(boolean validated, boolean hasInternet, boolean isStrictMode)
+                throws Exception {
+            final int uid = Process.myUid();
+            establish(makeLinkProperties(), uid, uidRangesForUids(uid), validated, hasInternet,
+                    isStrictMode);
+        }
+
+        public void establishForMyUid() throws Exception {
+            establishForMyUid(makeLinkProperties());
+        }
+
+        public void sendLinkProperties(LinkProperties lp) {
+            mMockNetworkAgent.sendLinkProperties(lp);
+        }
+
+        public void disconnect() {
+            if (mMockNetworkAgent != null) {
+                mMockNetworkAgent.disconnect();
+                updateState(NetworkInfo.DetailedState.DISCONNECTED, "disconnect");
+            }
+            mAgentRegistered = false;
+            setUids(null);
+            // Remove NET_CAPABILITY_INTERNET or MockNetworkAgent will refuse to connect later on.
+            mNetworkCapabilities.removeCapability(NET_CAPABILITY_INTERNET);
+            mInterface = null;
+        }
+
+        @Override
+        public void startLegacyVpnRunner() {
+            mStartLegacyVpnCv.open();
+        }
+
+        public void expectStartLegacyVpnRunner() {
+            assertTrue("startLegacyVpnRunner not called after " + TIMEOUT_MS + " ms",
+                    mStartLegacyVpnCv.block(TIMEOUT_MS));
+
+            // startLegacyVpn calls stopVpnRunnerPrivileged, which will open mStopVpnRunnerCv, just
+            // before calling startLegacyVpnRunner. Restore mStopVpnRunnerCv, so the test can expect
+            // that the VpnRunner is stopped and immediately restarted by calling
+            // expectStartLegacyVpnRunner() and expectStopVpnRunnerPrivileged() back-to-back.
+            mStopVpnRunnerCv = new ConditionVariable();
+        }
+
+        @Override
+        public void stopVpnRunnerPrivileged() {
+            if (mVpnRunner != null) {
+                super.stopVpnRunnerPrivileged();
+                disconnect();
+                mStartLegacyVpnCv = new ConditionVariable();
+            }
+            mVpnRunner = null;
+            mStopVpnRunnerCv.open();
+        }
+
+        public void expectStopVpnRunnerPrivileged() {
+            assertTrue("stopVpnRunnerPrivileged not called after " + TIMEOUT_MS + " ms",
+                    mStopVpnRunnerCv.block(TIMEOUT_MS));
+        }
+
+        @Override
+        public synchronized UnderlyingNetworkInfo getUnderlyingNetworkInfo() {
+            if (mUnderlyingNetworkInfo != null) return mUnderlyingNetworkInfo;
+
+            return super.getUnderlyingNetworkInfo();
+        }
+
+        private synchronized void setUnderlyingNetworkInfo(
+                UnderlyingNetworkInfo underlyingNetworkInfo) {
+            mUnderlyingNetworkInfo = underlyingNetworkInfo;
+        }
+    }
+
+    private UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
+        return ranges.stream().map(
+                r -> new UidRangeParcel(r.start, r.stop)).toArray(UidRangeParcel[]::new);
+    }
+
+    private VpnManagerService makeVpnManagerService() {
+        final VpnManagerService.Dependencies deps = new VpnManagerService.Dependencies() {
+            public int getCallingUid() {
+                return mDeps.getCallingUid();
+            }
+
+            public HandlerThread makeHandlerThread() {
+                return mVMSHandlerThread;
+            }
+
+            @Override
+            public VpnProfileStore getVpnProfileStore() {
+                return mVpnProfileStore;
+            }
+
+            public INetd getNetd() {
+                return mMockNetd;
+            }
+
+            public INetworkManagementService getINetworkManagementService() {
+                return mNetworkManagementService;
+            }
+        };
+        return new VpnManagerService(mServiceContext, deps);
+    }
+
+    private void assertVpnTransportInfo(NetworkCapabilities nc, int type) {
+        assertNotNull(nc);
+        final TransportInfo ti = nc.getTransportInfo();
+        assertTrue("VPN TransportInfo is not a VpnTransportInfo: " + ti,
+                ti instanceof VpnTransportInfo);
+        assertEquals(type, ((VpnTransportInfo) ti).getType());
+
+    }
+
+    private void processBroadcast(Intent intent) {
+        mServiceContext.sendBroadcast(intent);
+        HandlerUtils.waitForIdle(mVMSHandlerThread, TIMEOUT_MS);
+        waitForIdle();
+    }
+
+    private void mockVpn(int uid) {
+        synchronized (mVpnManagerService.mVpns) {
+            int userId = UserHandle.getUserId(uid);
+            mMockVpn = new MockVpn(userId);
+            // Every running user always has a Vpn in the mVpns array, even if no VPN is running.
+            mVpnManagerService.mVpns.put(userId, mMockVpn);
+        }
+    }
+
+    private void mockUidNetworkingBlocked() {
+        doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1))
+        ).when(mNetworkPolicyManager).isUidNetworkingBlocked(anyInt(), anyBoolean());
+    }
+
+    private boolean isUidBlocked(int blockedReasons, boolean meteredNetwork) {
+        final int blockedOnAllNetworksReason = (blockedReasons & ~BLOCKED_METERED_REASON_MASK);
+        if (blockedOnAllNetworksReason != BLOCKED_REASON_NONE) {
+            return true;
+        }
+        if (meteredNetwork) {
+            return blockedReasons != BLOCKED_REASON_NONE;
+        }
+        return false;
+    }
+
+    private void setBlockedReasonChanged(int blockedReasons) {
+        mBlockedReasons = blockedReasons;
+        mPolicyCallback.onUidBlockedReasonChanged(Process.myUid(), blockedReasons);
+    }
+
+    private Nat464Xlat getNat464Xlat(NetworkAgentWrapper mna) {
+        return mService.getNetworkAgentInfoForNetwork(mna.getNetwork()).clatd;
+    }
+
+    private static class WrappedMultinetworkPolicyTracker extends MultinetworkPolicyTracker {
+        volatile boolean mConfigRestrictsAvoidBadWifi;
+        volatile int mConfigMeteredMultipathPreference;
+
+        WrappedMultinetworkPolicyTracker(Context c, Handler h, Runnable r) {
+            super(c, h, r);
+        }
+
+        @Override
+        public boolean configRestrictsAvoidBadWifi() {
+            return mConfigRestrictsAvoidBadWifi;
+        }
+
+        @Override
+        public int configMeteredMultipathPreference() {
+            return mConfigMeteredMultipathPreference;
+        }
+    }
+
+    /**
+     * Wait up to TIMEOUT_MS for {@code conditionVariable} to open.
+     * Fails if TIMEOUT_MS goes by before {@code conditionVariable} opens.
+     */
+    static private void waitFor(ConditionVariable conditionVariable) {
+        if (conditionVariable.block(TIMEOUT_MS)) {
+            return;
+        }
+        fail("ConditionVariable was blocked for more than " + TIMEOUT_MS + "ms");
+    }
+
+    private <T> T doAsUid(final int uid, @NonNull final Supplier<T> what) {
+        when(mDeps.getCallingUid()).thenReturn(uid);
+        try {
+            return what.get();
+        } finally {
+            returnRealCallingUid();
+        }
+    }
+
+    private void doAsUid(final int uid, @NonNull final Runnable what) {
+        doAsUid(uid, () -> {
+            what.run(); return Void.TYPE;
+        });
+    }
+
+    private void registerNetworkCallbackAsUid(NetworkRequest request, NetworkCallback callback,
+            int uid) {
+        doAsUid(uid, () -> {
+            mCm.registerNetworkCallback(request, callback);
+        });
+    }
+
+    private void registerDefaultNetworkCallbackAsUid(@NonNull final NetworkCallback callback,
+            final int uid) {
+        doAsUid(uid, () -> {
+            mCm.registerDefaultNetworkCallback(callback);
+            waitForIdle();
+        });
+    }
+
+    private interface ExceptionalRunnable {
+        void run() throws Exception;
+    }
+
+    private void withPermission(String permission, ExceptionalRunnable r) throws Exception {
+        if (mServiceContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
+            r.run();
+            return;
+        }
+        try {
+            mServiceContext.setPermission(permission, PERMISSION_GRANTED);
+            r.run();
+        } finally {
+            mServiceContext.setPermission(permission, PERMISSION_DENIED);
+        }
+    }
+
+    private static final int PRIMARY_USER = 0;
+    private static final int SECONDARY_USER = 10;
+    private static final int TERTIARY_USER = 11;
+    private static final UidRange PRIMARY_UIDRANGE =
+            UidRange.createForUser(UserHandle.of(PRIMARY_USER));
+    private static final int APP1_UID = UserHandle.getUid(PRIMARY_USER, 10100);
+    private static final int APP2_UID = UserHandle.getUid(PRIMARY_USER, 10101);
+    private static final int VPN_UID = UserHandle.getUid(PRIMARY_USER, 10043);
+    private static final UserInfo PRIMARY_USER_INFO = new UserInfo(PRIMARY_USER, "",
+            UserInfo.FLAG_PRIMARY);
+    private static final UserHandle PRIMARY_USER_HANDLE = new UserHandle(PRIMARY_USER);
+    private static final UserHandle SECONDARY_USER_HANDLE = new UserHandle(SECONDARY_USER);
+    private static final UserHandle TERTIARY_USER_HANDLE = new UserHandle(TERTIARY_USER);
+
+    private static final int RESTRICTED_USER = 1;
+    private static final UserInfo RESTRICTED_USER_INFO = new UserInfo(RESTRICTED_USER, "",
+            UserInfo.FLAG_RESTRICTED);
+    static {
+        RESTRICTED_USER_INFO.restrictedProfileParentId = PRIMARY_USER;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mNetIdManager = new TestNetIdManager();
+
+        mContext = InstrumentationRegistry.getContext();
+
+        MockitoAnnotations.initMocks(this);
+
+        when(mUserManager.getAliveUsers()).thenReturn(Arrays.asList(PRIMARY_USER_INFO));
+        when(mUserManager.getUserHandles(anyBoolean())).thenReturn(
+                Arrays.asList(PRIMARY_USER_HANDLE));
+        when(mUserManager.getUserInfo(PRIMARY_USER)).thenReturn(PRIMARY_USER_INFO);
+        // canHaveRestrictedProfile does not take a userId. It applies to the userId of the context
+        // it was started from, i.e., PRIMARY_USER.
+        when(mUserManager.canHaveRestrictedProfile()).thenReturn(true);
+        when(mUserManager.getUserInfo(RESTRICTED_USER)).thenReturn(RESTRICTED_USER_INFO);
+
+        final ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.targetSdkVersion = Build.VERSION_CODES.Q;
+        when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), any()))
+                .thenReturn(applicationInfo);
+        when(mPackageManager.getTargetSdkVersion(anyString()))
+                .thenReturn(applicationInfo.targetSdkVersion);
+        when(mSystemConfigManager.getSystemPermissionUids(anyString())).thenReturn(new int[0]);
+
+        // InstrumentationTestRunner prepares a looper, but AndroidJUnitRunner does not.
+        // http://b/25897652 .
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+        mockDefaultPackages();
+        mockHasSystemFeature(FEATURE_WIFI, true);
+        mockHasSystemFeature(FEATURE_WIFI_DIRECT, true);
+        doReturn(true).when(mTelephonyManager).isDataCapable();
+
+        FakeSettingsProvider.clearSettingsProvider();
+        mServiceContext = new MockContext(InstrumentationRegistry.getContext(),
+                new FakeSettingsProvider());
+        mServiceContext.setUseRegisteredHandlers(true);
+        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
+        mServiceContext.setPermission(NETWORK_STACK, PERMISSION_GRANTED);
+        mServiceContext.setPermission(CONTROL_OEM_PAID_NETWORK_PREFERENCE, PERMISSION_GRANTED);
+        mServiceContext.setPermission(PACKET_KEEPALIVE_OFFLOAD, PERMISSION_GRANTED);
+        mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_GRANTED);
+
+        mAlarmManagerThread = new HandlerThread("TestAlarmManager");
+        mAlarmManagerThread.start();
+        initAlarmManager(mAlarmManager, mAlarmManagerThread.getThreadHandler());
+
+        mCsHandlerThread = new HandlerThread("TestConnectivityService");
+        mVMSHandlerThread = new HandlerThread("TestVpnManagerService");
+        mDeps = makeDependencies();
+        returnRealCallingUid();
+        mService = new ConnectivityService(mServiceContext,
+                mMockDnsResolver,
+                mock(IpConnectivityLog.class),
+                mMockNetd,
+                mDeps);
+        mService.mLingerDelayMs = TEST_LINGER_DELAY_MS;
+        mService.mNascentDelayMs = TEST_NASCENT_DELAY_MS;
+        verify(mDeps).makeMultinetworkPolicyTracker(any(), any(), any());
+
+        final ArgumentCaptor<NetworkPolicyCallback> policyCallbackCaptor =
+                ArgumentCaptor.forClass(NetworkPolicyCallback.class);
+        verify(mNetworkPolicyManager).registerNetworkPolicyCallback(any(),
+                policyCallbackCaptor.capture());
+        mPolicyCallback = policyCallbackCaptor.getValue();
+
+        // Create local CM before sending system ready so that we can answer
+        // getSystemService() correctly.
+        mCm = new WrappedConnectivityManager(InstrumentationRegistry.getContext(), mService);
+        mService.systemReadyInternal();
+        mVpnManagerService = makeVpnManagerService();
+        mVpnManagerService.systemReady();
+        mockVpn(Process.myUid());
+        mCm.bindProcessToNetwork(null);
+        mQosCallbackTracker = mock(QosCallbackTracker.class);
+
+        // Ensure that the default setting for Captive Portals is used for most tests
+        setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT);
+        setAlwaysOnNetworks(false);
+        setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com");
+    }
+
+    private void returnRealCallingUid() {
+        doAnswer((invocationOnMock) -> Binder.getCallingUid()).when(mDeps).getCallingUid();
+    }
+
+    private ConnectivityService.Dependencies makeDependencies() {
+        doReturn(false).when(mSystemProperties).getBoolean("ro.radio.noril", false);
+        final ConnectivityService.Dependencies deps = mock(ConnectivityService.Dependencies.class);
+        doReturn(mCsHandlerThread).when(deps).makeHandlerThread();
+        doReturn(mNetIdManager).when(deps).makeNetIdManager();
+        doReturn(mNetworkStack).when(deps).getNetworkStack();
+        doReturn(mSystemProperties).when(deps).getSystemProperties();
+        doReturn(mProxyTracker).when(deps).makeProxyTracker(any(), any());
+        doReturn(true).when(deps).queryUserAccess(anyInt(), any(), any());
+        doAnswer(inv -> {
+            mPolicyTracker = new WrappedMultinetworkPolicyTracker(
+                    inv.getArgument(0), inv.getArgument(1), inv.getArgument(2));
+            return mPolicyTracker;
+        }).when(deps).makeMultinetworkPolicyTracker(any(), any(), any());
+        doReturn(true).when(deps).getCellular464XlatEnabled();
+        doAnswer(inv ->
+            new LocationPermissionChecker(inv.getArgument(0)) {
+                @Override
+                protected int getCurrentUser() {
+                    return runAsShell(CREATE_USERS, () -> super.getCurrentUser());
+                }
+            }).when(deps).makeLocationPermissionChecker(any());
+
+        doReturn(60000).when(mResources).getInteger(R.integer.config_networkTransitionTimeout);
+        doReturn("").when(mResources).getString(R.string.config_networkCaptivePortalServerUrl);
+        doReturn(new String[]{ WIFI_WOL_IFNAME }).when(mResources).getStringArray(
+                R.array.config_wakeonlan_supported_interfaces);
+        doReturn(new String[] { "0,1", "1,3" }).when(mResources).getStringArray(
+                R.array.config_networkSupportedKeepaliveCount);
+        doReturn(new String[0]).when(mResources).getStringArray(
+                R.array.config_networkNotifySwitches);
+        doReturn(new int[]{10, 11, 12, 14, 15}).when(mResources).getIntArray(
+                R.array.config_protectedNetworks);
+        // We don't test the actual notification value strings, so just return an empty array.
+        // It doesn't matter what the values are as long as it's not null.
+        doReturn(new String[0]).when(mResources).getStringArray(R.array.network_switch_type_name);
+
+        doReturn(R.array.config_networkSupportedKeepaliveCount).when(mResources)
+                .getIdentifier(eq("config_networkSupportedKeepaliveCount"), eq("array"), any());
+        doReturn(R.array.network_switch_type_name).when(mResources)
+                .getIdentifier(eq("network_switch_type_name"), eq("array"), any());
+
+
+        final ConnectivityResources connRes = mock(ConnectivityResources.class);
+        doReturn(mResources).when(connRes).get();
+        doReturn(connRes).when(deps).getResources(any());
+
+        final Context mockResContext = mock(Context.class);
+        doReturn(mResources).when(mockResContext).getResources();
+        ConnectivityResources.setResourcesContextForTest(mockResContext);
+
+        doAnswer(inv -> {
+            final PendingIntent a = inv.getArgument(0);
+            final PendingIntent b = inv.getArgument(1);
+            return runAsShell(GET_INTENT_SENDER_INTENT, () -> a.intentFilterEquals(b));
+        }).when(deps).intentFilterEquals(any(), any());
+
+        return deps;
+    }
+
+    private static void initAlarmManager(final AlarmManager am, final Handler alarmHandler) {
+        doAnswer(inv -> {
+            final long when = inv.getArgument(1);
+            final WakeupMessage wakeupMsg = inv.getArgument(3);
+            final Handler handler = inv.getArgument(4);
+
+            long delayMs = when - SystemClock.elapsedRealtime();
+            if (delayMs < 0) delayMs = 0;
+            if (delayMs > UNREASONABLY_LONG_ALARM_WAIT_MS) {
+                fail("Attempting to send msg more than " + UNREASONABLY_LONG_ALARM_WAIT_MS
+                        + "ms into the future: " + delayMs);
+            }
+            alarmHandler.postDelayed(() -> handler.post(wakeupMsg::onAlarm), wakeupMsg /* token */,
+                    delayMs);
+
+            return null;
+        }).when(am).setExact(eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), anyLong(), anyString(),
+                any(WakeupMessage.class), any());
+
+        doAnswer(inv -> {
+            final WakeupMessage wakeupMsg = inv.getArgument(0);
+            alarmHandler.removeCallbacksAndMessages(wakeupMsg /* token */);
+            return null;
+        }).when(am).cancel(any(WakeupMessage.class));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        unregisterDefaultNetworkCallbacks();
+        maybeTearDownEnterpriseNetwork();
+        setAlwaysOnNetworks(false);
+        if (mCellNetworkAgent != null) {
+            mCellNetworkAgent.disconnect();
+            mCellNetworkAgent = null;
+        }
+        if (mWiFiNetworkAgent != null) {
+            mWiFiNetworkAgent.disconnect();
+            mWiFiNetworkAgent = null;
+        }
+        if (mEthernetNetworkAgent != null) {
+            mEthernetNetworkAgent.disconnect();
+            mEthernetNetworkAgent = null;
+        }
+
+        if (mQosCallbackMockHelper != null) {
+            mQosCallbackMockHelper.tearDown();
+            mQosCallbackMockHelper = null;
+        }
+        mMockVpn.disconnect();
+        waitForIdle();
+
+        FakeSettingsProvider.clearSettingsProvider();
+        ConnectivityResources.setResourcesContextForTest(null);
+
+        mCsHandlerThread.quitSafely();
+        mAlarmManagerThread.quitSafely();
+    }
+
+    private void mockDefaultPackages() throws Exception {
+        final String myPackageName = mContext.getPackageName();
+        final PackageInfo myPackageInfo = mContext.getPackageManager().getPackageInfo(
+                myPackageName, PackageManager.GET_PERMISSIONS);
+        when(mPackageManager.getPackagesForUid(Binder.getCallingUid())).thenReturn(
+                new String[] {myPackageName});
+        when(mPackageManager.getPackageInfoAsUser(eq(myPackageName), anyInt(),
+                eq(UserHandle.getCallingUserId()))).thenReturn(myPackageInfo);
+
+        when(mPackageManager.getInstalledPackages(eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn(
+                Arrays.asList(new PackageInfo[] {
+                        buildPackageInfo(/* SYSTEM */ false, APP1_UID),
+                        buildPackageInfo(/* SYSTEM */ false, APP2_UID),
+                        buildPackageInfo(/* SYSTEM */ false, VPN_UID)
+                }));
+
+        // Create a fake always-on VPN package.
+        final int userId = UserHandle.getCallingUserId();
+        final ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.targetSdkVersion = Build.VERSION_CODES.R;  // Always-on supported in N+.
+        when(mPackageManager.getApplicationInfoAsUser(eq(ALWAYS_ON_PACKAGE), anyInt(),
+                eq(userId))).thenReturn(applicationInfo);
+
+        // Minimal mocking to keep Vpn#isAlwaysOnPackageSupported happy.
+        ResolveInfo rInfo = new ResolveInfo();
+        rInfo.serviceInfo = new ServiceInfo();
+        rInfo.serviceInfo.metaData = new Bundle();
+        final List<ResolveInfo> services = Arrays.asList(new ResolveInfo[]{rInfo});
+        when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA),
+                eq(userId))).thenReturn(services);
+        when(mPackageManager.getPackageUidAsUser(TEST_PACKAGE_NAME, userId))
+                .thenReturn(Process.myUid());
+        when(mPackageManager.getPackageUidAsUser(ALWAYS_ON_PACKAGE, userId))
+                .thenReturn(VPN_UID);
+    }
+
+    private void verifyActiveNetwork(int transport) {
+        // Test getActiveNetworkInfo()
+        assertNotNull(mCm.getActiveNetworkInfo());
+        assertEquals(transportToLegacyType(transport), mCm.getActiveNetworkInfo().getType());
+        // Test getActiveNetwork()
+        assertNotNull(mCm.getActiveNetwork());
+        assertEquals(mCm.getActiveNetwork(), mCm.getActiveNetworkForUid(Process.myUid()));
+        if (!NetworkCapabilities.isValidTransport(transport)) {
+            throw new IllegalStateException("Unknown transport " + transport);
+        }
+        switch (transport) {
+            case TRANSPORT_WIFI:
+                assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+                break;
+            case TRANSPORT_CELLULAR:
+                assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+                break;
+            case TRANSPORT_ETHERNET:
+                assertEquals(mEthernetNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+                break;
+            default:
+                break;
+        }
+        // Test getNetworkInfo(Network)
+        assertNotNull(mCm.getNetworkInfo(mCm.getActiveNetwork()));
+        assertEquals(transportToLegacyType(transport),
+                mCm.getNetworkInfo(mCm.getActiveNetwork()).getType());
+        assertNotNull(mCm.getActiveNetworkInfoForUid(Process.myUid()));
+        // Test getNetworkCapabilities(Network)
+        assertNotNull(mCm.getNetworkCapabilities(mCm.getActiveNetwork()));
+        assertTrue(mCm.getNetworkCapabilities(mCm.getActiveNetwork()).hasTransport(transport));
+    }
+
+    private void verifyNoNetwork() {
+        waitForIdle();
+        // Test getActiveNetworkInfo()
+        assertNull(mCm.getActiveNetworkInfo());
+        // Test getActiveNetwork()
+        assertNull(mCm.getActiveNetwork());
+        assertNull(mCm.getActiveNetworkForUid(Process.myUid()));
+        // Test getAllNetworks()
+        assertEmpty(mCm.getAllNetworks());
+        assertEmpty(mCm.getAllNetworkStateSnapshots());
+    }
+
+    /**
+     * Class to simplify expecting broadcasts using BroadcastInterceptingContext.
+     * Ensures that the receiver is unregistered after the expected broadcast is received. This
+     * cannot be done in the BroadcastReceiver itself because BroadcastInterceptingContext runs
+     * the receivers' receive method while iterating over the list of receivers, and unregistering
+     * the receiver during iteration throws ConcurrentModificationException.
+     */
+    private class ExpectedBroadcast extends CompletableFuture<Intent>  {
+        private final BroadcastReceiver mReceiver;
+
+        ExpectedBroadcast(BroadcastReceiver receiver) {
+            mReceiver = receiver;
+        }
+
+        public Intent expectBroadcast(int timeoutMs) throws Exception {
+            try {
+                return get(timeoutMs, TimeUnit.MILLISECONDS);
+            } catch (TimeoutException e) {
+                fail("Expected broadcast not received after " + timeoutMs + " ms");
+                return null;
+            } finally {
+                mServiceContext.unregisterReceiver(mReceiver);
+            }
+        }
+
+        public Intent expectBroadcast() throws Exception {
+            return expectBroadcast(BROADCAST_TIMEOUT_MS);
+        }
+
+        public void expectNoBroadcast(int timeoutMs) throws Exception {
+            waitForIdle();
+            try {
+                final Intent intent = get(timeoutMs, TimeUnit.MILLISECONDS);
+                fail("Unexpected broadcast: " + intent.getAction() + " " + intent.getExtras());
+            } catch (TimeoutException expected) {
+            } finally {
+                mServiceContext.unregisterReceiver(mReceiver);
+            }
+        }
+    }
+
+    /** Expects that {@code count} CONNECTIVITY_ACTION broadcasts are received. */
+    private ExpectedBroadcast registerConnectivityBroadcast(final int count) {
+        return registerConnectivityBroadcastThat(count, intent -> true);
+    }
+
+    private ExpectedBroadcast registerConnectivityBroadcastThat(final int count,
+            @NonNull final Predicate<Intent> filter) {
+        final IntentFilter intentFilter = new IntentFilter(CONNECTIVITY_ACTION);
+        // AtomicReference allows receiver to access expected even though it is constructed later.
+        final AtomicReference<ExpectedBroadcast> expectedRef = new AtomicReference<>();
+        final BroadcastReceiver receiver = new BroadcastReceiver() {
+            private int mRemaining = count;
+            public void onReceive(Context context, Intent intent) {
+                final int type = intent.getIntExtra(EXTRA_NETWORK_TYPE, -1);
+                final NetworkInfo ni = intent.getParcelableExtra(EXTRA_NETWORK_INFO);
+                Log.d(TAG, "Received CONNECTIVITY_ACTION type=" + type + " ni=" + ni);
+                if (!filter.test(intent)) return;
+                if (--mRemaining == 0) {
+                    expectedRef.get().complete(intent);
+                }
+            }
+        };
+        final ExpectedBroadcast expected = new ExpectedBroadcast(receiver);
+        expectedRef.set(expected);
+        mServiceContext.registerReceiver(receiver, intentFilter);
+        return expected;
+    }
+
+    private boolean extraInfoInBroadcastHasExpectedNullness(NetworkInfo ni) {
+        final DetailedState state = ni.getDetailedState();
+        if (state == DetailedState.CONNECTED && ni.getExtraInfo() == null) return false;
+        // Expect a null extraInfo if the network is CONNECTING, because a CONNECTIVITY_ACTION
+        // broadcast with a state of CONNECTING only happens due to legacy VPN lockdown, which also
+        // nulls out extraInfo.
+        if (state == DetailedState.CONNECTING && ni.getExtraInfo() != null) return false;
+        // Can't make any assertions about DISCONNECTED broadcasts. When a network actually
+        // disconnects, disconnectAndDestroyNetwork sets its state to DISCONNECTED and its extraInfo
+        // to null. But if the DISCONNECTED broadcast is just simulated by LegacyTypeTracker due to
+        // a network switch, extraInfo will likely be populated.
+        // This is likely a bug in CS, but likely not one we can fix without impacting apps.
+        return true;
+    }
+
+    private ExpectedBroadcast expectConnectivityAction(int type, NetworkInfo.DetailedState state) {
+        return registerConnectivityBroadcastThat(1, intent -> {
+            final int actualType = intent.getIntExtra(EXTRA_NETWORK_TYPE, -1);
+            final NetworkInfo ni = intent.getParcelableExtra(EXTRA_NETWORK_INFO);
+            return type == actualType
+                    && state == ni.getDetailedState()
+                    && extraInfoInBroadcastHasExpectedNullness(ni);
+        });
+    }
+
+    @Test
+    public void testNetworkTypes() {
+        // Ensure that our mocks for the networkAttributes config variable work as expected. If they
+        // don't, then tests that depend on CONNECTIVITY_ACTION broadcasts for these network types
+        // will fail. Failing here is much easier to debug.
+        assertTrue(mCm.isNetworkSupported(TYPE_WIFI));
+        assertTrue(mCm.isNetworkSupported(TYPE_MOBILE));
+        assertTrue(mCm.isNetworkSupported(TYPE_MOBILE_MMS));
+        assertTrue(mCm.isNetworkSupported(TYPE_MOBILE_FOTA));
+        assertFalse(mCm.isNetworkSupported(TYPE_PROXY));
+
+        // Check that TYPE_ETHERNET is supported. Unlike the asserts above, which only validate our
+        // mocks, this assert exercises the ConnectivityService code path that ensures that
+        // TYPE_ETHERNET is supported if the ethernet service is running.
+        assertTrue(mCm.isNetworkSupported(TYPE_ETHERNET));
+    }
+
+    @Test
+    public void testNetworkFeature() throws Exception {
+        // Connect the cell agent and wait for the connected broadcast.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_SUPL);
+        ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED);
+        mCellNetworkAgent.connect(true);
+        b.expectBroadcast();
+
+        // Build legacy request for SUPL.
+        final NetworkCapabilities legacyCaps = new NetworkCapabilities();
+        legacyCaps.addTransportType(TRANSPORT_CELLULAR);
+        legacyCaps.addCapability(NET_CAPABILITY_SUPL);
+        final NetworkRequest legacyRequest = new NetworkRequest(legacyCaps, TYPE_MOBILE_SUPL,
+                ConnectivityManager.REQUEST_ID_UNSET, NetworkRequest.Type.REQUEST);
+
+        // File request, withdraw it and make sure no broadcast is sent
+        b = registerConnectivityBroadcast(1);
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.requestNetwork(legacyRequest, callback);
+        callback.expectCallback(CallbackEntry.AVAILABLE, mCellNetworkAgent);
+        mCm.unregisterNetworkCallback(callback);
+        b.expectNoBroadcast(800);  // 800ms long enough to at least flake if this is sent
+
+        // Disconnect the network and expect mobile disconnected broadcast.
+        b = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        mCellNetworkAgent.disconnect();
+        b.expectBroadcast();
+    }
+
+    @Test
+    public void testLingering() throws Exception {
+        verifyNoNetwork();
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        assertNull(mCm.getActiveNetworkInfo());
+        assertNull(mCm.getActiveNetwork());
+        // Test bringing up validated cellular.
+        ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED);
+        mCellNetworkAgent.connect(true);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        assertLength(2, mCm.getAllNetworks());
+        assertTrue(mCm.getAllNetworks()[0].equals(mCm.getActiveNetwork()) ||
+                mCm.getAllNetworks()[1].equals(mCm.getActiveNetwork()));
+        assertTrue(mCm.getAllNetworks()[0].equals(mWiFiNetworkAgent.getNetwork()) ||
+                mCm.getAllNetworks()[1].equals(mWiFiNetworkAgent.getNetwork()));
+        // Test bringing up validated WiFi.
+        b = registerConnectivityBroadcast(2);
+        mWiFiNetworkAgent.connect(true);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        assertLength(2, mCm.getAllNetworks());
+        assertTrue(mCm.getAllNetworks()[0].equals(mCm.getActiveNetwork()) ||
+                mCm.getAllNetworks()[1].equals(mCm.getActiveNetwork()));
+        assertTrue(mCm.getAllNetworks()[0].equals(mCellNetworkAgent.getNetwork()) ||
+                mCm.getAllNetworks()[1].equals(mCellNetworkAgent.getNetwork()));
+        // Test cellular linger timeout.
+        mCellNetworkAgent.expectDisconnected();
+        waitForIdle();
+        assertLength(1, mCm.getAllNetworks());
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        assertLength(1, mCm.getAllNetworks());
+        assertEquals(mCm.getAllNetworks()[0], mCm.getActiveNetwork());
+        // Test WiFi disconnect.
+        b = registerConnectivityBroadcast(1);
+        mWiFiNetworkAgent.disconnect();
+        b.expectBroadcast();
+        verifyNoNetwork();
+    }
+
+    /**
+     * Verify a newly created network will be inactive instead of torn down even if no one is
+     * requesting.
+     */
+    @Test
+    public void testNewNetworkInactive() throws Exception {
+        // Create a callback that monitoring the testing network.
+        final TestNetworkCallback listenCallback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(new NetworkRequest.Builder().build(), listenCallback);
+
+        // 1. Create a network that is not requested by anyone, and does not satisfy any of the
+        // default requests. Verify that the network will be inactive instead of torn down.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connectWithoutInternet();
+        listenCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        listenCallback.assertNoCallback();
+
+        // Verify that the network will be torn down after nascent expiry. A small period of time
+        // is added in case of flakiness.
+        final int nascentTimeoutMs =
+                mService.mNascentDelayMs + mService.mNascentDelayMs / 4;
+        listenCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent, nascentTimeoutMs);
+
+        // 2. Create a network that is satisfied by a request comes later.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connectWithoutInternet();
+        listenCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        final TestNetworkCallback wifiCallback = new TestNetworkCallback();
+        mCm.requestNetwork(wifiRequest, wifiCallback);
+        wifiCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+
+        // Verify that the network will be kept since the request is still satisfied. And is able
+        // to get disconnected as usual if the request is released after the nascent timer expires.
+        listenCallback.assertNoCallback(nascentTimeoutMs);
+        mCm.unregisterNetworkCallback(wifiCallback);
+        listenCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        // 3. Create a network that is satisfied by a request comes later.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connectWithoutInternet();
+        listenCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        mCm.requestNetwork(wifiRequest, wifiCallback);
+        wifiCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+
+        // Verify that the network will still be torn down after the request gets removed.
+        mCm.unregisterNetworkCallback(wifiCallback);
+        listenCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        // There is no need to ensure that LOSING is never sent in the common case that the
+        // network immediately satisfies a request that was already present, because it is already
+        // verified anywhere whenever {@code TestNetworkCallback#expectAvailable*} is called.
+
+        mCm.unregisterNetworkCallback(listenCallback);
+    }
+
+    /**
+     * Verify a newly created network will be inactive and switch to background if only background
+     * request is satisfied.
+     */
+    @Test
+    public void testNewNetworkInactive_bgNetwork() throws Exception {
+        // Create a callback that monitoring the wifi network.
+        final TestNetworkCallback wifiListenCallback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build(), wifiListenCallback);
+
+        // Create callbacks that can monitor background and foreground mobile networks.
+        // This is done by granting using background networks permission before registration. Thus,
+        // the service will not add {@code NET_CAPABILITY_FOREGROUND} by default.
+        grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid());
+        final TestNetworkCallback bgMobileListenCallback = new TestNetworkCallback();
+        final TestNetworkCallback fgMobileListenCallback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build(), bgMobileListenCallback);
+        mCm.registerNetworkCallback(new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_FOREGROUND).build(), fgMobileListenCallback);
+
+        // Connect wifi, which satisfies default request.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        wifiListenCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+
+        // Connect a cellular network, verify that satisfies only the background callback.
+        setAlwaysOnNetworks(true);
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        bgMobileListenCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        fgMobileListenCallback.assertNoCallback();
+        assertFalse(isForegroundNetwork(mCellNetworkAgent));
+
+        mCellNetworkAgent.disconnect();
+        bgMobileListenCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        fgMobileListenCallback.assertNoCallback();
+
+        mCm.unregisterNetworkCallback(wifiListenCallback);
+        mCm.unregisterNetworkCallback(bgMobileListenCallback);
+        mCm.unregisterNetworkCallback(fgMobileListenCallback);
+    }
+
+    @Test
+    public void testValidatedCellularOutscoresUnvalidatedWiFi() throws Exception {
+        // Test bringing up unvalidated WiFi
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        ExpectedBroadcast b = registerConnectivityBroadcast(1);
+        mWiFiNetworkAgent.connect(false);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test bringing up unvalidated cellular
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(false);
+        waitForIdle();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test cellular disconnect.
+        mCellNetworkAgent.disconnect();
+        waitForIdle();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test bringing up validated cellular
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        b = registerConnectivityBroadcast(2);
+        mCellNetworkAgent.connect(true);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test cellular disconnect.
+        b = registerConnectivityBroadcast(2);
+        mCellNetworkAgent.disconnect();
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test WiFi disconnect.
+        b = registerConnectivityBroadcast(1);
+        mWiFiNetworkAgent.disconnect();
+        b.expectBroadcast();
+        verifyNoNetwork();
+    }
+
+    @Test
+    public void testUnvalidatedWifiOutscoresUnvalidatedCellular() throws Exception {
+        // Test bringing up unvalidated cellular.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        ExpectedBroadcast b = registerConnectivityBroadcast(1);
+        mCellNetworkAgent.connect(false);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test bringing up unvalidated WiFi.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        b = registerConnectivityBroadcast(2);
+        mWiFiNetworkAgent.connect(false);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test WiFi disconnect.
+        b = registerConnectivityBroadcast(2);
+        mWiFiNetworkAgent.disconnect();
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test cellular disconnect.
+        b = registerConnectivityBroadcast(1);
+        mCellNetworkAgent.disconnect();
+        b.expectBroadcast();
+        verifyNoNetwork();
+    }
+
+    @Test
+    public void testUnlingeringDoesNotValidate() throws Exception {
+        // Test bringing up unvalidated WiFi.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        ExpectedBroadcast b = registerConnectivityBroadcast(1);
+        mWiFiNetworkAgent.connect(false);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        // Test bringing up validated cellular.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        b = registerConnectivityBroadcast(2);
+        mCellNetworkAgent.connect(true);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        // Test cellular disconnect.
+        b = registerConnectivityBroadcast(2);
+        mCellNetworkAgent.disconnect();
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Unlingering a network should not cause it to be marked as validated.
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+    }
+
+    @Test
+    public void testCellularOutscoresWeakWifi() throws Exception {
+        // Test bringing up validated cellular.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        ExpectedBroadcast b = registerConnectivityBroadcast(1);
+        mCellNetworkAgent.connect(true);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test bringing up validated WiFi.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        b = registerConnectivityBroadcast(2);
+        mWiFiNetworkAgent.connect(true);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test WiFi getting really weak.
+        b = registerConnectivityBroadcast(2);
+        mWiFiNetworkAgent.adjustScore(-11);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test WiFi restoring signal strength.
+        b = registerConnectivityBroadcast(2);
+        mWiFiNetworkAgent.adjustScore(11);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+    }
+
+    @Test
+    public void testReapingNetwork() throws Exception {
+        // Test bringing up WiFi without NET_CAPABILITY_INTERNET.
+        // Expect it to be torn down immediately because it satisfies no requests.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connectWithoutInternet();
+        mWiFiNetworkAgent.expectDisconnected();
+        // Test bringing up cellular without NET_CAPABILITY_INTERNET.
+        // Expect it to be torn down immediately because it satisfies no requests.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mCellNetworkAgent.connectWithoutInternet();
+        mCellNetworkAgent.expectDisconnected();
+        // Test bringing up validated WiFi.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        final ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
+        mWiFiNetworkAgent.connect(true);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test bringing up unvalidated cellular.
+        // Expect it to be torn down because it could never be the highest scoring network
+        // satisfying the default request even if it validated.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(false);
+        mCellNetworkAgent.expectDisconnected();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.disconnect();
+        mWiFiNetworkAgent.expectDisconnected();
+    }
+
+    @Test
+    public void testCellularFallback() throws Exception {
+        // Test bringing up validated cellular.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        ExpectedBroadcast b = registerConnectivityBroadcast(1);
+        mCellNetworkAgent.connect(true);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Test bringing up validated WiFi.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        b = registerConnectivityBroadcast(2);
+        mWiFiNetworkAgent.connect(true);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Reevaluate WiFi (it'll instantly fail DNS).
+        b = registerConnectivityBroadcast(2);
+        assertTrue(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        mCm.reportBadNetwork(mWiFiNetworkAgent.getNetwork());
+        // Should quickly fall back to Cellular.
+        b.expectBroadcast();
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Reevaluate cellular (it'll instantly fail DNS).
+        b = registerConnectivityBroadcast(2);
+        assertTrue(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        mCm.reportBadNetwork(mCellNetworkAgent.getNetwork());
+        // Should quickly fall back to WiFi.
+        b.expectBroadcast();
+        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        verifyActiveNetwork(TRANSPORT_WIFI);
+    }
+
+    @Test
+    public void testWiFiFallback() throws Exception {
+        // Test bringing up unvalidated WiFi.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        ExpectedBroadcast b = registerConnectivityBroadcast(1);
+        mWiFiNetworkAgent.connect(false);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        // Test bringing up validated cellular.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        b = registerConnectivityBroadcast(2);
+        mCellNetworkAgent.connect(true);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+        // Reevaluate cellular (it'll instantly fail DNS).
+        b = registerConnectivityBroadcast(2);
+        assertTrue(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        mCm.reportBadNetwork(mCellNetworkAgent.getNetwork());
+        // Should quickly fall back to WiFi.
+        b.expectBroadcast();
+        assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        verifyActiveNetwork(TRANSPORT_WIFI);
+    }
+
+    @Test
+    public void testRequiresValidation() {
+        assertTrue(NetworkMonitorUtils.isValidationRequired(
+                mCm.getDefaultRequest().networkCapabilities));
+    }
+
+    /**
+     * Utility NetworkCallback for testing. The caller must explicitly test for all the callbacks
+     * this class receives, by calling expectCallback() exactly once each time a callback is
+     * received. assertNoCallback may be called at any time.
+     */
+    private class TestNetworkCallback extends TestableNetworkCallback {
+        TestNetworkCallback() {
+            super(TEST_CALLBACK_TIMEOUT_MS);
+        }
+
+        @Override
+        public void assertNoCallback() {
+            // TODO: better support this use case in TestableNetworkCallback
+            waitForIdle();
+            assertNoCallback(0 /* timeout */);
+        }
+
+        @Override
+        public <T extends CallbackEntry> T expectCallback(final KClass<T> type, final HasNetwork n,
+                final long timeoutMs) {
+            final T callback = super.expectCallback(type, n, timeoutMs);
+            if (callback instanceof CallbackEntry.Losing) {
+                // TODO : move this to the specific test(s) needing this rather than here.
+                final CallbackEntry.Losing losing = (CallbackEntry.Losing) callback;
+                final int maxMsToLive = losing.getMaxMsToLive();
+                String msg = String.format(
+                        "Invalid linger time value %d, must be between %d and %d",
+                        maxMsToLive, 0, mService.mLingerDelayMs);
+                assertTrue(msg, 0 <= maxMsToLive && maxMsToLive <= mService.mLingerDelayMs);
+            }
+            return callback;
+        }
+    }
+
+    // Can't be part of TestNetworkCallback because "cannot be declared static; static methods can
+    // only be declared in a static or top level type".
+    static void assertNoCallbacks(TestNetworkCallback ... callbacks) {
+        for (TestNetworkCallback c : callbacks) {
+            c.assertNoCallback();
+        }
+    }
+
+    static void expectOnLost(TestNetworkAgentWrapper network, TestNetworkCallback ... callbacks) {
+        for (TestNetworkCallback c : callbacks) {
+            c.expectCallback(CallbackEntry.LOST, network);
+        }
+    }
+
+    static void expectAvailableCallbacksUnvalidatedWithSpecifier(TestNetworkAgentWrapper network,
+            NetworkSpecifier specifier, TestNetworkCallback ... callbacks) {
+        for (TestNetworkCallback c : callbacks) {
+            c.expectCallback(CallbackEntry.AVAILABLE, network);
+            c.expectCapabilitiesThat(network, (nc) ->
+                    !nc.hasCapability(NET_CAPABILITY_VALIDATED)
+                            && Objects.equals(specifier, nc.getNetworkSpecifier()));
+            c.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, network);
+            c.expectCallback(CallbackEntry.BLOCKED_STATUS, network);
+        }
+    }
+
+    @Test
+    public void testStateChangeNetworkCallbacks() throws Exception {
+        final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback();
+        final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback();
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest genericRequest = new NetworkRequest.Builder()
+                .clearCapabilities().build();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.registerNetworkCallback(genericRequest, genericNetworkCallback);
+        mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
+        mCm.registerNetworkCallback(cellRequest, cellNetworkCallback);
+
+        // Test unvalidated networks
+        ExpectedBroadcast b = registerConnectivityBroadcast(1);
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(false);
+        genericNetworkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        cellNetworkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        b.expectBroadcast();
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        // This should not trigger spurious onAvailable() callbacks, b/21762680.
+        mCellNetworkAgent.adjustScore(-1);
+        waitForIdle();
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        b = registerConnectivityBroadcast(2);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        genericNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        wifiNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        b.expectBroadcast();
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        b = registerConnectivityBroadcast(2);
+        mWiFiNetworkAgent.disconnect();
+        genericNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        wifiNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        b.expectBroadcast();
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        b = registerConnectivityBroadcast(1);
+        mCellNetworkAgent.disconnect();
+        genericNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        b.expectBroadcast();
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        // Test validated networks
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        genericNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        // This should not trigger spurious onAvailable() callbacks, b/21762680.
+        mCellNetworkAgent.adjustScore(-1);
+        waitForIdle();
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        genericNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        genericNetworkCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        genericNetworkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        wifiNetworkCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        mWiFiNetworkAgent.disconnect();
+        genericNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        wifiNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+        mCellNetworkAgent.disconnect();
+        genericNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+    }
+
+    private void doNetworkCallbacksSanitizationTest(boolean sanitized) throws Exception {
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        final TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        mCm.registerNetworkCallback(wifiRequest, callback);
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+
+        final LinkProperties newLp = new LinkProperties();
+        final Uri capportUrl = Uri.parse("https://capport.example.com/api");
+        final CaptivePortalData capportData = new CaptivePortalData.Builder()
+                .setCaptive(true).build();
+
+        final Uri expectedCapportUrl = sanitized ? null : capportUrl;
+        newLp.setCaptivePortalApiUrl(capportUrl);
+        mWiFiNetworkAgent.sendLinkProperties(newLp);
+        callback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp ->
+                Objects.equals(expectedCapportUrl, lp.getCaptivePortalApiUrl()));
+        defaultCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp ->
+                Objects.equals(expectedCapportUrl, lp.getCaptivePortalApiUrl()));
+
+        final CaptivePortalData expectedCapportData = sanitized ? null : capportData;
+        mWiFiNetworkAgent.notifyCapportApiDataChanged(capportData);
+        callback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp ->
+                Objects.equals(expectedCapportData, lp.getCaptivePortalData()));
+        defaultCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp ->
+                Objects.equals(expectedCapportData, lp.getCaptivePortalData()));
+
+        final LinkProperties lp = mCm.getLinkProperties(mWiFiNetworkAgent.getNetwork());
+        assertEquals(expectedCapportUrl, lp.getCaptivePortalApiUrl());
+        assertEquals(expectedCapportData, lp.getCaptivePortalData());
+    }
+
+    @Test
+    public void networkCallbacksSanitizationTest_Sanitize() throws Exception {
+        mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                PERMISSION_DENIED);
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED);
+        doNetworkCallbacksSanitizationTest(true /* sanitized */);
+    }
+
+    @Test
+    public void networkCallbacksSanitizationTest_NoSanitize_NetworkStack() throws Exception {
+        mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                PERMISSION_GRANTED);
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED);
+        doNetworkCallbacksSanitizationTest(false /* sanitized */);
+    }
+
+    @Test
+    public void networkCallbacksSanitizationTest_NoSanitize_Settings() throws Exception {
+        mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                PERMISSION_DENIED);
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+        doNetworkCallbacksSanitizationTest(false /* sanitized */);
+    }
+
+    @Test
+    public void testOwnerUidCannotChange() throws Exception {
+        final NetworkCapabilities ncTemplate = new NetworkCapabilities();
+        final int originalOwnerUid = Process.myUid();
+        ncTemplate.setOwnerUid(originalOwnerUid);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(),
+                ncTemplate);
+        mWiFiNetworkAgent.connect(false);
+        waitForIdle();
+
+        // Send ConnectivityService an update to the mWiFiNetworkAgent's capabilities that changes
+        // the owner UID and an unrelated capability.
+        NetworkCapabilities agentCapabilities = mWiFiNetworkAgent.getNetworkCapabilities();
+        assertEquals(originalOwnerUid, agentCapabilities.getOwnerUid());
+        agentCapabilities.setOwnerUid(42);
+        assertFalse(agentCapabilities.hasCapability(NET_CAPABILITY_NOT_CONGESTED));
+        agentCapabilities.addCapability(NET_CAPABILITY_NOT_CONGESTED);
+        mWiFiNetworkAgent.setNetworkCapabilities(agentCapabilities, true);
+        waitForIdle();
+
+        // Owner UIDs are not visible without location permission.
+        setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION,
+                Manifest.permission.ACCESS_FINE_LOCATION);
+
+        // Check that the capability change has been applied but the owner UID is not modified.
+        NetworkCapabilities nc = mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork());
+        assertEquals(originalOwnerUid, nc.getOwnerUid());
+        assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_CONGESTED));
+    }
+
+    @Test
+    public void testMultipleLingering() throws Exception {
+        // This test would be flaky with the default 120ms timer: that is short enough that
+        // lingered networks are torn down before assertions can be run. We don't want to mock the
+        // lingering timer to keep the WakeupMessage logic realistic: this has already proven useful
+        // in detecting races.
+        mService.mLingerDelayMs = 300;
+
+        NetworkRequest request = new NetworkRequest.Builder()
+                .clearCapabilities().addCapability(NET_CAPABILITY_NOT_METERED)
+                .build();
+        TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+
+        TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
+
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mEthernetNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+
+        mCellNetworkAgent.connect(true);
+        callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        mWiFiNetworkAgent.connect(true);
+        // We get AVAILABLE on wifi when wifi connects and satisfies our unmetered request.
+        // We then get LOSING when wifi validates and cell is outscored.
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        // TODO: Investigate sending validated before losing.
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        mEthernetNetworkAgent.connect(true);
+        callback.expectAvailableCallbacksUnvalidated(mEthernetNetworkAgent);
+        // TODO: Investigate sending validated before losing.
+        callback.expectCallback(CallbackEntry.LOSING, mWiFiNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mEthernetNetworkAgent);
+        defaultCallback.expectAvailableDoubleValidatedCallbacks(mEthernetNetworkAgent);
+        assertEquals(mEthernetNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        mEthernetNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent);
+        defaultCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent);
+        defaultCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        for (int i = 0; i < 4; i++) {
+            TestNetworkAgentWrapper oldNetwork, newNetwork;
+            if (i % 2 == 0) {
+                mWiFiNetworkAgent.adjustScore(-15);
+                oldNetwork = mWiFiNetworkAgent;
+                newNetwork = mCellNetworkAgent;
+            } else {
+                mWiFiNetworkAgent.adjustScore(15);
+                oldNetwork = mCellNetworkAgent;
+                newNetwork = mWiFiNetworkAgent;
+
+            }
+            callback.expectCallback(CallbackEntry.LOSING, oldNetwork);
+            // TODO: should we send an AVAILABLE callback to newNetwork, to indicate that it is no
+            // longer lingering?
+            defaultCallback.expectAvailableCallbacksValidated(newNetwork);
+            assertEquals(newNetwork.getNetwork(), mCm.getActiveNetwork());
+        }
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Verify that if a network no longer satisfies a request, we send LOST and not LOSING, even
+        // if the network is still up.
+        mWiFiNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED);
+        // We expect a notification about the capabilities change, and nothing else.
+        defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_NOT_METERED, mWiFiNetworkAgent);
+        defaultCallback.assertNoCallback();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        // Wifi no longer satisfies our listen, which is for an unmetered network.
+        // But because its score is 55, it's still up (and the default network).
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Disconnect our test networks.
+        mWiFiNetworkAgent.disconnect();
+        defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+        mCellNetworkAgent.disconnect();
+        defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        waitForIdle();
+        assertEquals(null, mCm.getActiveNetwork());
+
+        mCm.unregisterNetworkCallback(callback);
+        waitForIdle();
+
+        // Check that a network is only lingered or torn down if it would not satisfy a request even
+        // if it validated.
+        request = new NetworkRequest.Builder().clearCapabilities().build();
+        callback = new TestNetworkCallback();
+
+        mCm.registerNetworkCallback(request, callback);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(false);   // Score: 10
+        callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        defaultCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        // Bring up wifi with a score of 20.
+        // Cell stays up because it would satisfy the default request if it validated.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);   // Score: 20
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        // Bring up wifi, then validate it. Previous versions would immediately tear down cell, but
+        // it's arguably correct to linger it, since it was the default network before it validated.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        // TODO: Investigate sending validated before losing.
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        defaultCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        mCellNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        waitForIdle();
+        assertEquals(null, mCm.getActiveNetwork());
+
+        // If a network is lingering, and we add and remove a request from it, resume lingering.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        // TODO: Investigate sending validated before losing.
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        NetworkCallback noopCallback = new NetworkCallback();
+        mCm.requestNetwork(cellRequest, noopCallback);
+        // TODO: should this cause an AVAILABLE callback, to indicate that the network is no longer
+        // lingering?
+        mCm.unregisterNetworkCallback(noopCallback);
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+
+        // Similar to the above: lingering can start even after the lingered request is removed.
+        // Disconnect wifi and switch to cell.
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        // Cell is now the default network. Pin it with a cell-specific request.
+        noopCallback = new NetworkCallback();  // Can't reuse NetworkCallbacks. http://b/20701525
+        mCm.requestNetwork(cellRequest, noopCallback);
+
+        // Now connect wifi, and expect it to become the default network.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        callback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+        defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+        // The default request is lingering on cell, but nothing happens to cell, and we send no
+        // callbacks for it, because it's kept up by cellRequest.
+        callback.assertNoCallback();
+        // Now unregister cellRequest and expect cell to start lingering.
+        mCm.unregisterNetworkCallback(noopCallback);
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+
+        // Let linger run its course.
+        callback.assertNoCallback();
+        final int lingerTimeoutMs = mService.mLingerDelayMs + mService.mLingerDelayMs / 4;
+        callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent, lingerTimeoutMs);
+
+        // Register a TRACK_DEFAULT request and check that it does not affect lingering.
+        TestNetworkCallback trackDefaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(trackDefaultCallback);
+        trackDefaultCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
+        mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
+        mEthernetNetworkAgent.connect(true);
+        callback.expectAvailableCallbacksUnvalidated(mEthernetNetworkAgent);
+        callback.expectCallback(CallbackEntry.LOSING, mWiFiNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mEthernetNetworkAgent);
+        trackDefaultCallback.expectAvailableDoubleValidatedCallbacks(mEthernetNetworkAgent);
+        defaultCallback.expectAvailableDoubleValidatedCallbacks(mEthernetNetworkAgent);
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        // Let linger run its course.
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent, lingerTimeoutMs);
+
+        // Clean up.
+        mEthernetNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent);
+        defaultCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent);
+        trackDefaultCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent);
+
+        mCm.unregisterNetworkCallback(callback);
+        mCm.unregisterNetworkCallback(defaultCallback);
+        mCm.unregisterNetworkCallback(trackDefaultCallback);
+    }
+
+    private void grantUsingBackgroundNetworksPermissionForUid(final int uid) throws Exception {
+        grantUsingBackgroundNetworksPermissionForUid(uid, mContext.getPackageName());
+    }
+
+    private void grantUsingBackgroundNetworksPermissionForUid(
+            final int uid, final String packageName) throws Exception {
+        when(mPackageManager.getPackageInfo(
+                eq(packageName), eq(GET_PERMISSIONS | MATCH_ANY_USER)))
+                .thenReturn(buildPackageInfo(true /* hasSystemPermission */, uid));
+        mService.mPermissionMonitor.onPackageAdded(packageName, uid);
+    }
+
+    @Test
+    public void testNetworkGoesIntoBackgroundAfterLinger() throws Exception {
+        setAlwaysOnNetworks(true);
+        grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid());
+        NetworkRequest request = new NetworkRequest.Builder()
+                .clearCapabilities()
+                .build();
+        TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+
+        TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+
+        mCellNetworkAgent.connect(true);
+        callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        // Wifi comes up and cell lingers.
+        mWiFiNetworkAgent.connect(true);
+        defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+
+        // File a request for cellular, then release it.
+        NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        NetworkCallback noopCallback = new NetworkCallback();
+        mCm.requestNetwork(cellRequest, noopCallback);
+        mCm.unregisterNetworkCallback(noopCallback);
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+
+        // Let linger run its course.
+        callback.assertNoCallback();
+        final int lingerTimeoutMs = TEST_LINGER_DELAY_MS + TEST_LINGER_DELAY_MS / 4;
+        callback.expectCapabilitiesWithout(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent,
+                lingerTimeoutMs);
+
+        // Clean up.
+        mCm.unregisterNetworkCallback(defaultCallback);
+        mCm.unregisterNetworkCallback(callback);
+    }
+
+    private NativeNetworkConfig nativeNetworkConfigPhysical(int netId, int permission) {
+        return new NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL, permission,
+                /*secure=*/ false, VpnManager.TYPE_VPN_NONE);
+    }
+
+    private NativeNetworkConfig nativeNetworkConfigVpn(int netId, boolean secure, int vpnType) {
+        return new NativeNetworkConfig(netId, NativeNetworkType.VIRTUAL, INetd.PERMISSION_NONE,
+                secure, vpnType);
+    }
+
+    @Test
+    public void testNetworkAgentCallbacks() throws Exception {
+        // Keeps track of the order of events that happen in this test.
+        final LinkedBlockingQueue<String> eventOrder = new LinkedBlockingQueue<>();
+
+        final NetworkRequest request = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        final AtomicReference<Network> wifiNetwork = new AtomicReference<>();
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+
+        // Expectations for state when various callbacks fire. These expectations run on the handler
+        // thread and not on the test thread because they need to prevent the handler thread from
+        // advancing while they examine state.
+
+        // 1. When onCreated fires, netd has been told to create the network.
+        mWiFiNetworkAgent.setCreatedCallback(() -> {
+            eventOrder.offer("onNetworkCreated");
+            wifiNetwork.set(mWiFiNetworkAgent.getNetwork());
+            assertNotNull(wifiNetwork.get());
+            try {
+                verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                        wifiNetwork.get().getNetId(), INetd.PERMISSION_NONE));
+            } catch (RemoteException impossible) {
+                fail();
+            }
+        });
+
+        // 2. onNetworkUnwanted isn't precisely ordered with respect to any particular events. Just
+        //    check that it is fired at some point after disconnect.
+        mWiFiNetworkAgent.setUnwantedCallback(() -> eventOrder.offer("onNetworkUnwanted"));
+
+        // 3. While the teardown timer is running, connectivity APIs report the network is gone, but
+        //    netd has not yet been told to destroy it.
+        final Runnable duringTeardown = () -> {
+            eventOrder.offer("timePasses");
+            assertNull(mCm.getLinkProperties(wifiNetwork.get()));
+            try {
+                verify(mMockNetd, never()).networkDestroy(wifiNetwork.get().getNetId());
+            } catch (RemoteException impossible) {
+                fail();
+            }
+        };
+
+        // 4. After onNetworkDisconnected is called, connectivity APIs report the network is gone,
+        // and netd has been told to destroy it.
+        mWiFiNetworkAgent.setDisconnectedCallback(() -> {
+            eventOrder.offer("onNetworkDisconnected");
+            assertNull(mCm.getLinkProperties(wifiNetwork.get()));
+            try {
+                verify(mMockNetd).networkDestroy(wifiNetwork.get().getNetId());
+            } catch (RemoteException impossible) {
+                fail();
+            }
+        });
+
+        // Connect a network, and file a request for it after it has come up, to ensure the nascent
+        // timer is cleared and the test does not have to wait for it. Filing the request after the
+        // network has come up is necessary because ConnectivityService does not appear to clear the
+        // nascent timer if the first request satisfied by the network was filed before the network
+        // connected.
+        // TODO: fix this bug, file the request before connecting, and remove the waitForIdle.
+        mWiFiNetworkAgent.connectWithoutInternet();
+        waitForIdle();
+        mCm.requestNetwork(request, callback);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+
+        // Set teardown delay and make sure CS has processed it.
+        mWiFiNetworkAgent.getNetworkAgent().setTeardownDelayMillis(300);
+        waitForIdle();
+
+        // Post the duringTeardown lambda to the handler so it fires while teardown is in progress.
+        // The delay must be long enough it will run after the unregisterNetworkCallback has torn
+        // down the network and started the teardown timer, and short enough that the lambda is
+        // scheduled to run before the teardown timer.
+        final Handler h = new Handler(mCsHandlerThread.getLooper());
+        h.postDelayed(duringTeardown, 150);
+
+        // Disconnect the network and check that events happened in the right order.
+        mCm.unregisterNetworkCallback(callback);
+        assertEquals("onNetworkCreated", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertEquals("onNetworkUnwanted", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertEquals("timePasses", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertEquals("onNetworkDisconnected", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        mCm.unregisterNetworkCallback(callback);
+    }
+
+    @Test
+    public void testExplicitlySelected() throws Exception {
+        NetworkRequest request = new NetworkRequest.Builder()
+                .clearCapabilities().addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+
+        // Bring up validated cell.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        // Bring up unvalidated wifi with explicitlySelected=true.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.explicitlySelected(true, false);
+        mWiFiNetworkAgent.connect(false);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+
+        // Cell Remains the default.
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Lower wifi's score to below than cell, and check that it doesn't disconnect because
+        // it's explicitly selected.
+        mWiFiNetworkAgent.adjustScore(-40);
+        mWiFiNetworkAgent.adjustScore(40);
+        callback.assertNoCallback();
+
+        // If the user chooses yes on the "No Internet access, stay connected?" dialog, we switch to
+        // wifi even though it's unvalidated.
+        mCm.setAcceptUnvalidated(mWiFiNetworkAgent.getNetwork(), true, false);
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Disconnect wifi, and then reconnect, again with explicitlySelected=true.
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.explicitlySelected(true, false);
+        mWiFiNetworkAgent.connect(false);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+
+        // If the user chooses no on the "No Internet access, stay connected?" dialog, we ask the
+        // network to disconnect.
+        mCm.setAcceptUnvalidated(mWiFiNetworkAgent.getNetwork(), false, false);
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        // Reconnect, again with explicitlySelected=true, but this time validate.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.explicitlySelected(true, false);
+        mWiFiNetworkAgent.connect(true);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
+        mEthernetNetworkAgent.connect(true);
+        callback.expectAvailableCallbacksUnvalidated(mEthernetNetworkAgent);
+        callback.expectCallback(CallbackEntry.LOSING, mWiFiNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mEthernetNetworkAgent);
+        assertEquals(mEthernetNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        callback.assertNoCallback();
+
+        // Disconnect wifi, and then reconnect as if the user had selected "yes, don't ask again"
+        // (i.e., with explicitlySelected=true and acceptUnvalidated=true). Expect to switch to
+        // wifi immediately.
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.explicitlySelected(true, true);
+        mWiFiNetworkAgent.connect(false);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        callback.expectCallback(CallbackEntry.LOSING, mEthernetNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        mEthernetNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent);
+
+        // Disconnect and reconnect with explicitlySelected=false and acceptUnvalidated=true.
+        // Check that the network is not scored specially and that the device prefers cell data.
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.explicitlySelected(false, true);
+        mWiFiNetworkAgent.connect(false);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Clean up.
+        mWiFiNetworkAgent.disconnect();
+        mCellNetworkAgent.disconnect();
+
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+    }
+
+    private void tryNetworkFactoryRequests(int capability) throws Exception {
+        // Verify NOT_RESTRICTED is set appropriately
+        final NetworkCapabilities nc = new NetworkRequest.Builder().addCapability(capability)
+                .build().networkCapabilities;
+        if (capability == NET_CAPABILITY_CBS || capability == NET_CAPABILITY_DUN
+                || capability == NET_CAPABILITY_EIMS || capability == NET_CAPABILITY_FOTA
+                || capability == NET_CAPABILITY_IA || capability == NET_CAPABILITY_IMS
+                || capability == NET_CAPABILITY_RCS || capability == NET_CAPABILITY_XCAP
+                || capability == NET_CAPABILITY_VSIM || capability == NET_CAPABILITY_BIP
+                || capability == NET_CAPABILITY_ENTERPRISE) {
+            assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        } else {
+            assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        }
+
+        NetworkCapabilities filter = new NetworkCapabilities();
+        filter.addTransportType(TRANSPORT_CELLULAR);
+        filter.addCapability(capability);
+        // Add NOT_VCN_MANAGED capability into filter unconditionally since some requests will add
+        // NOT_VCN_MANAGED automatically but not for NetworkCapabilities,
+        // see {@code NetworkCapabilities#deduceNotVcnManagedCapability} for more details.
+        filter.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        final HandlerThread handlerThread = new HandlerThread("testNetworkFactoryRequests");
+        handlerThread.start();
+        final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "testFactory", filter, mCsHandlerThread);
+        testFactory.setScoreFilter(45);
+        testFactory.register();
+
+        final NetworkCallback networkCallback;
+        if (capability != NET_CAPABILITY_INTERNET) {
+            // If the capability passed in argument is part of the default request, then the
+            // factory will see the default request. Otherwise the filter will prevent the
+            // factory from seeing it. In that case, add a request so it can be tested.
+            assertFalse(testFactory.getMyStartRequested());
+            NetworkRequest request = new NetworkRequest.Builder().addCapability(capability).build();
+            networkCallback = new NetworkCallback();
+            mCm.requestNetwork(request, networkCallback);
+        } else {
+            networkCallback = null;
+        }
+        testFactory.expectRequestAdd();
+        testFactory.assertRequestCountEquals(1);
+        assertTrue(testFactory.getMyStartRequested());
+
+        // Now bring in a higher scored network.
+        TestNetworkAgentWrapper testAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        // When testAgent connects, because of its score (50 legacy int / cell transport)
+        // it will beat or equal the testFactory's offer, so the request will be removed.
+        // Note the agent as validated only if the capability is INTERNET, as it's the only case
+        // where it makes sense.
+        testAgent.connect(NET_CAPABILITY_INTERNET == capability /* validated */);
+        testAgent.addCapability(capability);
+        testFactory.expectRequestRemove();
+        testFactory.assertRequestCountEquals(0);
+        assertFalse(testFactory.getMyStartRequested());
+
+        // Add a request and make sure it's not sent to the factory, because the agent
+        // is satisfying it better.
+        final NetworkCallback cb = new ConnectivityManager.NetworkCallback();
+        mCm.requestNetwork(new NetworkRequest.Builder().addCapability(capability).build(), cb);
+        expectNoRequestChanged(testFactory);
+        testFactory.assertRequestCountEquals(0);
+        assertFalse(testFactory.getMyStartRequested());
+
+        // If using legacy scores, make the test agent weak enough to have the exact same score as
+        // the factory (50 for cell - 5 adjustment). Make sure the factory doesn't see the request.
+        // If not using legacy score, this is a no-op and the "same score removes request" behavior
+        // has already been tested above.
+        testAgent.adjustScore(-5);
+        expectNoRequestChanged(testFactory);
+        assertFalse(testFactory.getMyStartRequested());
+
+        // Make the test agent weak enough that the factory will see the two requests (the one that
+        // was just sent, and either the default one or the one sent at the top of this test if
+        // the default won't be seen).
+        testAgent.setScore(new NetworkScore.Builder().setLegacyInt(2).setExiting(true).build());
+        testFactory.expectRequestAdds(2);
+        testFactory.assertRequestCountEquals(2);
+        assertTrue(testFactory.getMyStartRequested());
+
+        // Now unregister and make sure the request is removed.
+        mCm.unregisterNetworkCallback(cb);
+        testFactory.expectRequestRemove();
+
+        // Bring in a bunch of requests.
+        assertEquals(1, testFactory.getMyRequestCount());
+        ConnectivityManager.NetworkCallback[] networkCallbacks =
+                new ConnectivityManager.NetworkCallback[10];
+        for (int i = 0; i< networkCallbacks.length; i++) {
+            networkCallbacks[i] = new ConnectivityManager.NetworkCallback();
+            NetworkRequest.Builder builder = new NetworkRequest.Builder();
+            builder.addCapability(capability);
+            mCm.requestNetwork(builder.build(), networkCallbacks[i]);
+        }
+        testFactory.expectRequestAdds(10);
+        testFactory.assertRequestCountEquals(11); // +1 for the default/test specific request
+        assertTrue(testFactory.getMyStartRequested());
+
+        // Remove the requests.
+        for (int i = 0; i < networkCallbacks.length; i++) {
+            mCm.unregisterNetworkCallback(networkCallbacks[i]);
+        }
+        testFactory.expectRequestRemoves(10);
+        testFactory.assertRequestCountEquals(1);
+        assertTrue(testFactory.getMyStartRequested());
+
+        // Adjust the agent score up again. Expect the request to be withdrawn.
+        testAgent.setScore(new NetworkScore.Builder().setLegacyInt(50).build());
+        testFactory.expectRequestRemove();
+        testFactory.assertRequestCountEquals(0);
+        assertFalse(testFactory.getMyStartRequested());
+
+        // Drop the higher scored network.
+        testAgent.disconnect();
+        testFactory.expectRequestAdd();
+        testFactory.assertRequestCountEquals(1);
+        assertEquals(1, testFactory.getMyRequestCount());
+        assertTrue(testFactory.getMyStartRequested());
+
+        testFactory.terminate();
+        if (networkCallback != null) mCm.unregisterNetworkCallback(networkCallback);
+        handlerThread.quit();
+    }
+
+    @Test
+    public void testNetworkFactoryRequests() throws Exception {
+        tryNetworkFactoryRequests(NET_CAPABILITY_MMS);
+        tryNetworkFactoryRequests(NET_CAPABILITY_SUPL);
+        tryNetworkFactoryRequests(NET_CAPABILITY_DUN);
+        tryNetworkFactoryRequests(NET_CAPABILITY_FOTA);
+        tryNetworkFactoryRequests(NET_CAPABILITY_IMS);
+        tryNetworkFactoryRequests(NET_CAPABILITY_CBS);
+        tryNetworkFactoryRequests(NET_CAPABILITY_WIFI_P2P);
+        tryNetworkFactoryRequests(NET_CAPABILITY_IA);
+        tryNetworkFactoryRequests(NET_CAPABILITY_RCS);
+        tryNetworkFactoryRequests(NET_CAPABILITY_XCAP);
+        tryNetworkFactoryRequests(NET_CAPABILITY_ENTERPRISE);
+        tryNetworkFactoryRequests(NET_CAPABILITY_EIMS);
+        tryNetworkFactoryRequests(NET_CAPABILITY_NOT_METERED);
+        tryNetworkFactoryRequests(NET_CAPABILITY_INTERNET);
+        tryNetworkFactoryRequests(NET_CAPABILITY_TRUSTED);
+        tryNetworkFactoryRequests(NET_CAPABILITY_NOT_VPN);
+        tryNetworkFactoryRequests(NET_CAPABILITY_VSIM);
+        tryNetworkFactoryRequests(NET_CAPABILITY_BIP);
+        // Skipping VALIDATED and CAPTIVE_PORTAL as they're disallowed.
+    }
+
+    @Test
+    public void testRegisterIgnoringScore() throws Exception {
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(90).build());
+        mWiFiNetworkAgent.connect(true /* validated */);
+
+        // Make sure the factory sees the default network
+        final NetworkCapabilities filter = new NetworkCapabilities();
+        filter.addTransportType(TRANSPORT_CELLULAR);
+        filter.addCapability(NET_CAPABILITY_INTERNET);
+        filter.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        final HandlerThread handlerThread = new HandlerThread("testNetworkFactoryRequests");
+        handlerThread.start();
+        final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "testFactory", filter, mCsHandlerThread);
+        testFactory.register();
+
+        final MockNetworkFactory testFactoryAll = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "testFactoryAll", filter, mCsHandlerThread);
+        testFactoryAll.registerIgnoringScore();
+
+        // The regular test factory should not see the request, because WiFi is stronger than cell.
+        expectNoRequestChanged(testFactory);
+        // With ignoringScore though the request is seen.
+        testFactoryAll.expectRequestAdd();
+
+        // The legacy int will be ignored anyway, set the only other knob to true
+        mWiFiNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(110)
+                .setTransportPrimary(true).build());
+
+        expectNoRequestChanged(testFactory); // still not seeing the request
+        expectNoRequestChanged(testFactoryAll); // still seeing the request
+
+        mWiFiNetworkAgent.disconnect();
+    }
+
+    @Test
+    public void testNetworkFactoryUnregister() throws Exception {
+        // Make sure the factory sees the default network
+        final NetworkCapabilities filter = new NetworkCapabilities();
+        filter.addCapability(NET_CAPABILITY_INTERNET);
+        filter.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+
+        final HandlerThread handlerThread = new HandlerThread("testNetworkFactoryRequests");
+        handlerThread.start();
+
+        // Checks that calling setScoreFilter on a NetworkFactory immediately before closing it
+        // does not crash.
+        for (int i = 0; i < 100; i++) {
+            final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                    mServiceContext, "testFactory", filter, mCsHandlerThread);
+            // Register the factory and don't be surprised when the default request arrives.
+            testFactory.register();
+            testFactory.expectRequestAdd();
+
+            testFactory.setScoreFilter(42);
+            testFactory.terminate();
+
+            if (i % 2 == 0) {
+                try {
+                    testFactory.register();
+                    fail("Re-registering terminated NetworkFactory should throw");
+                } catch (IllegalStateException expected) {
+                }
+            }
+        }
+        handlerThread.quit();
+    }
+
+    @Test
+    public void testNoMutableNetworkRequests() throws Exception {
+        final PendingIntent pendingIntent = PendingIntent.getBroadcast(
+                mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE);
+        NetworkRequest request1 = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_VALIDATED)
+                .build();
+        NetworkRequest request2 = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL)
+                .build();
+
+        Class<IllegalArgumentException> expected = IllegalArgumentException.class;
+        assertThrows(expected, () -> mCm.requestNetwork(request1, new NetworkCallback()));
+        assertThrows(expected, () -> mCm.requestNetwork(request1, pendingIntent));
+        assertThrows(expected, () -> mCm.requestNetwork(request2, new NetworkCallback()));
+        assertThrows(expected, () -> mCm.requestNetwork(request2, pendingIntent));
+    }
+
+    @Test
+    public void testMMSonWiFi() throws Exception {
+        // Test bringing up cellular without MMS NetworkRequest gets reaped
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS);
+        mCellNetworkAgent.connectWithoutInternet();
+        mCellNetworkAgent.expectDisconnected();
+        waitForIdle();
+        assertEmpty(mCm.getAllNetworks());
+        verifyNoNetwork();
+
+        // Test bringing up validated WiFi.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        final ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
+        mWiFiNetworkAgent.connect(true);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+
+        // Register MMS NetworkRequest
+        NetworkRequest.Builder builder = new NetworkRequest.Builder();
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.requestNetwork(builder.build(), networkCallback);
+
+        // Test bringing up unvalidated cellular with MMS
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS);
+        mCellNetworkAgent.connectWithoutInternet();
+        networkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        verifyActiveNetwork(TRANSPORT_WIFI);
+
+        // Test releasing NetworkRequest disconnects cellular with MMS
+        mCm.unregisterNetworkCallback(networkCallback);
+        mCellNetworkAgent.expectDisconnected();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+    }
+
+    @Test
+    public void testMMSonCell() throws Exception {
+        // Test bringing up cellular without MMS
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED);
+        mCellNetworkAgent.connect(false);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+
+        // Register MMS NetworkRequest
+        NetworkRequest.Builder builder = new NetworkRequest.Builder();
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.requestNetwork(builder.build(), networkCallback);
+
+        // Test bringing up MMS cellular network
+        TestNetworkAgentWrapper
+                mmsNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mmsNetworkAgent.addCapability(NET_CAPABILITY_MMS);
+        mmsNetworkAgent.connectWithoutInternet();
+        networkCallback.expectAvailableCallbacksUnvalidated(mmsNetworkAgent);
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+
+        // Test releasing MMS NetworkRequest does not disconnect main cellular NetworkAgent
+        mCm.unregisterNetworkCallback(networkCallback);
+        mmsNetworkAgent.expectDisconnected();
+        verifyActiveNetwork(TRANSPORT_CELLULAR);
+    }
+
+    @Test
+    public void testPartialConnectivity() throws Exception {
+        // Register network callback.
+        NetworkRequest request = new NetworkRequest.Builder()
+                .clearCapabilities().addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+
+        // Bring up validated mobile data.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        // Bring up wifi with partial connectivity.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connectWithPartialConnectivity();
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, mWiFiNetworkAgent);
+
+        // Mobile data should be the default network.
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        callback.assertNoCallback();
+
+        // With HTTPS probe disabled, NetworkMonitor should pass the network validation with http
+        // probe.
+        mWiFiNetworkAgent.setNetworkPartialValid(false /* isStrictMode */);
+        // If the user chooses yes to use this partial connectivity wifi, switch the default
+        // network to wifi and check if wifi becomes valid or not.
+        mCm.setAcceptPartialConnectivity(mWiFiNetworkAgent.getNetwork(), true /* accept */,
+                false /* always */);
+        // If user accepts partial connectivity network,
+        // NetworkMonitor#setAcceptPartialConnectivity() should be called too.
+        waitForIdle();
+        verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity();
+
+        // Need a trigger point to let NetworkMonitor tell ConnectivityService that network is
+        // validated.
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true);
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        NetworkCapabilities nc = callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED,
+                mWiFiNetworkAgent);
+        assertTrue(nc.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY));
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Disconnect and reconnect wifi with partial connectivity again.
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connectWithPartialConnectivity();
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, mWiFiNetworkAgent);
+
+        // Mobile data should be the default network.
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // If the user chooses no, disconnect wifi immediately.
+        mCm.setAcceptPartialConnectivity(mWiFiNetworkAgent.getNetwork(), false/* accept */,
+                false /* always */);
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        // If user accepted partial connectivity before, and device reconnects to that network
+        // again, but now the network has full connectivity. The network shouldn't contain
+        // NET_CAPABILITY_PARTIAL_CONNECTIVITY.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        // acceptUnvalidated is also used as setting for accepting partial networks.
+        mWiFiNetworkAgent.explicitlySelected(true /* explicitlySelected */,
+                true /* acceptUnvalidated */);
+        mWiFiNetworkAgent.connect(true);
+
+        // If user accepted partial connectivity network before,
+        // NetworkMonitor#setAcceptPartialConnectivity() will be called in
+        // ConnectivityService#updateNetworkInfo().
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity();
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        nc = callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        assertFalse(nc.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY));
+
+        // Wifi should be the default network.
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        // The user accepted partial connectivity and selected "don't ask again". Now the user
+        // reconnects to the partial connectivity network. Switch to wifi as soon as partial
+        // connectivity is detected.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.explicitlySelected(true /* explicitlySelected */,
+                true /* acceptUnvalidated */);
+        mWiFiNetworkAgent.connectWithPartialConnectivity();
+        // If user accepted partial connectivity network before,
+        // NetworkMonitor#setAcceptPartialConnectivity() will be called in
+        // ConnectivityService#updateNetworkInfo().
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity();
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        callback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, mWiFiNetworkAgent);
+        mWiFiNetworkAgent.setNetworkValid(false /* isStrictMode */);
+
+        // Need a trigger point to let NetworkMonitor tell ConnectivityService that network is
+        // validated.
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        // If the user accepted partial connectivity, and the device auto-reconnects to the partial
+        // connectivity network, it should contain both PARTIAL_CONNECTIVITY and VALIDATED.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.explicitlySelected(false /* explicitlySelected */,
+                true /* acceptUnvalidated */);
+
+        // NetworkMonitor will immediately (once the HTTPS probe fails...) report the network as
+        // valid, because ConnectivityService calls setAcceptPartialConnectivity before it calls
+        // notifyNetworkConnected.
+        mWiFiNetworkAgent.connectWithPartialValidConnectivity(false /* isStrictMode */);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity();
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        callback.expectCapabilitiesWith(
+                NET_CAPABILITY_PARTIAL_CONNECTIVITY | NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+    }
+
+    @Test
+    public void testCaptivePortalOnPartialConnectivity() throws Exception {
+        final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
+        final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
+        mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback);
+
+        final TestNetworkCallback validatedCallback = new TestNetworkCallback();
+        final NetworkRequest validatedRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_VALIDATED).build();
+        mCm.registerNetworkCallback(validatedRequest, validatedCallback);
+
+        // Bring up a network with a captive portal.
+        // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        String redirectUrl = "http://android.com/path";
+        mWiFiNetworkAgent.connectWithCaptivePortal(redirectUrl, false /* isStrictMode */);
+        captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), redirectUrl);
+
+        // Check that startCaptivePortalApp sends the expected command to NetworkMonitor.
+        mCm.startCaptivePortalApp(mWiFiNetworkAgent.getNetwork());
+        verify(mWiFiNetworkAgent.mNetworkMonitor, timeout(TIMEOUT_MS).times(1))
+                .launchCaptivePortalApp();
+
+        // Report that the captive portal is dismissed with partial connectivity, and check that
+        // callbacks are fired.
+        mWiFiNetworkAgent.setNetworkPartial();
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true);
+        waitForIdle();
+        captivePortalCallback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY,
+                mWiFiNetworkAgent);
+
+        // Report partial connectivity is accepted.
+        mWiFiNetworkAgent.setNetworkPartialValid(false /* isStrictMode */);
+        mCm.setAcceptPartialConnectivity(mWiFiNetworkAgent.getNetwork(), true /* accept */,
+                false /* always */);
+        waitForIdle();
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true);
+        captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        validatedCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
+        NetworkCapabilities nc =
+                validatedCallback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY,
+                mWiFiNetworkAgent);
+
+        mCm.unregisterNetworkCallback(captivePortalCallback);
+        mCm.unregisterNetworkCallback(validatedCallback);
+    }
+
+    @Test
+    public void testCaptivePortal() throws Exception {
+        final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
+        final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
+        mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback);
+
+        final TestNetworkCallback validatedCallback = new TestNetworkCallback();
+        final NetworkRequest validatedRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_VALIDATED).build();
+        mCm.registerNetworkCallback(validatedRequest, validatedCallback);
+
+        // Bring up a network with a captive portal.
+        // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        String firstRedirectUrl = "http://example.com/firstPath";
+        mWiFiNetworkAgent.connectWithCaptivePortal(firstRedirectUrl, false /* isStrictMode */);
+        captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), firstRedirectUrl);
+
+        // Take down network.
+        // Expect onLost callback.
+        mWiFiNetworkAgent.disconnect();
+        captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        // Bring up a network with a captive portal.
+        // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        String secondRedirectUrl = "http://example.com/secondPath";
+        mWiFiNetworkAgent.connectWithCaptivePortal(secondRedirectUrl, false /* isStrictMode */);
+        captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), secondRedirectUrl);
+
+        // Make captive portal disappear then revalidate.
+        // Expect onLost callback because network no longer provides NET_CAPABILITY_CAPTIVE_PORTAL.
+        mWiFiNetworkAgent.setNetworkValid(false /* isStrictMode */);
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true);
+        captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        // Expect NET_CAPABILITY_VALIDATED onAvailable callback.
+        validatedCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+
+        // Break network connectivity.
+        // Expect NET_CAPABILITY_VALIDATED onLost callback.
+        mWiFiNetworkAgent.setNetworkInvalid(false /* isStrictMode */);
+        mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), false);
+        validatedCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+    }
+
+    @Test
+    public void testCaptivePortalApp() throws Exception {
+        final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
+        final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
+        mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback);
+
+        final TestNetworkCallback validatedCallback = new TestNetworkCallback();
+        final NetworkRequest validatedRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_VALIDATED).build();
+        mCm.registerNetworkCallback(validatedRequest, validatedCallback);
+
+        // Bring up wifi.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        validatedCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        Network wifiNetwork = mWiFiNetworkAgent.getNetwork();
+
+        // Check that calling startCaptivePortalApp does nothing.
+        final int fastTimeoutMs = 100;
+        mCm.startCaptivePortalApp(wifiNetwork);
+        waitForIdle();
+        verify(mWiFiNetworkAgent.mNetworkMonitor, never()).launchCaptivePortalApp();
+        mServiceContext.expectNoStartActivityIntent(fastTimeoutMs);
+
+        // Turn into a captive portal.
+        mWiFiNetworkAgent.setNetworkPortal("http://example.com", false /* isStrictMode */);
+        mCm.reportNetworkConnectivity(wifiNetwork, false);
+        captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        validatedCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        // Check that startCaptivePortalApp sends the expected command to NetworkMonitor.
+        mCm.startCaptivePortalApp(wifiNetwork);
+        waitForIdle();
+        verify(mWiFiNetworkAgent.mNetworkMonitor).launchCaptivePortalApp();
+
+        // NetworkMonitor uses startCaptivePortal(Network, Bundle) (startCaptivePortalAppInternal)
+        final Bundle testBundle = new Bundle();
+        final String testKey = "testkey";
+        final String testValue = "testvalue";
+        testBundle.putString(testKey, testValue);
+        mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                PERMISSION_GRANTED);
+        mCm.startCaptivePortalApp(wifiNetwork, testBundle);
+        final Intent signInIntent = mServiceContext.expectStartActivityIntent(TIMEOUT_MS);
+        assertEquals(ACTION_CAPTIVE_PORTAL_SIGN_IN, signInIntent.getAction());
+        assertEquals(testValue, signInIntent.getStringExtra(testKey));
+
+        // Report that the captive portal is dismissed, and check that callbacks are fired
+        mWiFiNetworkAgent.setNetworkValid(false /* isStrictMode */);
+        mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid());
+        validatedCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
+        captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        mCm.unregisterNetworkCallback(validatedCallback);
+        mCm.unregisterNetworkCallback(captivePortalCallback);
+    }
+
+    @Test
+    public void testAvoidOrIgnoreCaptivePortals() throws Exception {
+        final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
+        final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
+        mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback);
+
+        final TestNetworkCallback validatedCallback = new TestNetworkCallback();
+        final NetworkRequest validatedRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_VALIDATED).build();
+        mCm.registerNetworkCallback(validatedRequest, validatedCallback);
+
+        setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID);
+        // Bring up a network with a captive portal.
+        // Expect it to fail to connect and not result in any callbacks.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        String firstRedirectUrl = "http://example.com/firstPath";
+
+        mWiFiNetworkAgent.connectWithCaptivePortal(firstRedirectUrl, false /* isStrictMode */);
+        mWiFiNetworkAgent.expectDisconnected();
+        mWiFiNetworkAgent.expectPreventReconnectReceived();
+
+        assertNoCallbacks(captivePortalCallback, validatedCallback);
+    }
+
+    @Test
+    public void testCaptivePortalApi() throws Exception {
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+        final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
+        final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
+        mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        final String redirectUrl = "http://example.com/firstPath";
+
+        mWiFiNetworkAgent.connectWithCaptivePortal(redirectUrl, false /* isStrictMode */);
+        captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+
+        final CaptivePortalData testData = new CaptivePortalData.Builder()
+                .setUserPortalUrl(Uri.parse(redirectUrl))
+                .setBytesRemaining(12345L)
+                .build();
+
+        mWiFiNetworkAgent.notifyCapportApiDataChanged(testData);
+
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> testData.equals(lp.getCaptivePortalData()));
+
+        final LinkProperties newLps = new LinkProperties();
+        newLps.setMtu(1234);
+        mWiFiNetworkAgent.sendLinkProperties(newLps);
+        // CaptivePortalData is not lost and unchanged when LPs are received from the NetworkAgent
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> testData.equals(lp.getCaptivePortalData()) && lp.getMtu() == 1234);
+    }
+
+    private TestNetworkCallback setupNetworkCallbackAndConnectToWifi() throws Exception {
+        // Grant NETWORK_SETTINGS permission to be able to receive LinkProperties change callbacks
+        // with sensitive (captive portal) data
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+        final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
+        final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
+        mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+
+        mWiFiNetworkAgent.connectWithCaptivePortal(TEST_REDIRECT_URL, false /* isStrictMode */);
+        captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        return captivePortalCallback;
+    }
+
+    private class CaptivePortalTestData {
+        CaptivePortalTestData(CaptivePortalData naPasspointData, CaptivePortalData capportData,
+                CaptivePortalData naOtherData, CaptivePortalData expectedMergedPasspointData,
+                CaptivePortalData expectedMergedOtherData) {
+            mNaPasspointData = naPasspointData;
+            mCapportData = capportData;
+            mNaOtherData = naOtherData;
+            mExpectedMergedPasspointData = expectedMergedPasspointData;
+            mExpectedMergedOtherData = expectedMergedOtherData;
+        }
+
+        public final CaptivePortalData mNaPasspointData;
+        public final CaptivePortalData mCapportData;
+        public final CaptivePortalData mNaOtherData;
+        public final CaptivePortalData mExpectedMergedPasspointData;
+        public final CaptivePortalData mExpectedMergedOtherData;
+
+    }
+
+    private CaptivePortalTestData setupCaptivePortalData() {
+        final CaptivePortalData capportData = new CaptivePortalData.Builder()
+                .setUserPortalUrl(Uri.parse(TEST_REDIRECT_URL))
+                .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_CAPPORT))
+                .setUserPortalUrl(Uri.parse(TEST_USER_PORTAL_API_URL_CAPPORT))
+                .setExpiryTime(1000000L)
+                .setBytesRemaining(12345L)
+                .build();
+
+        final CaptivePortalData naPasspointData = new CaptivePortalData.Builder()
+                .setBytesRemaining(80802L)
+                .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_NA_PASSPOINT),
+                        CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT)
+                .setUserPortalUrl(Uri.parse(TEST_TERMS_AND_CONDITIONS_URL_NA_PASSPOINT),
+                        CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT)
+                .setVenueFriendlyName(TEST_FRIENDLY_NAME).build();
+
+        final CaptivePortalData naOtherData = new CaptivePortalData.Builder()
+                .setBytesRemaining(80802L)
+                .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_NA_OTHER),
+                        CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER)
+                .setUserPortalUrl(Uri.parse(TEST_TERMS_AND_CONDITIONS_URL_NA_OTHER),
+                        CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER)
+                .setVenueFriendlyName(TEST_FRIENDLY_NAME).build();
+
+        final CaptivePortalData expectedMergedPasspointData = new CaptivePortalData.Builder()
+                .setUserPortalUrl(Uri.parse(TEST_REDIRECT_URL))
+                .setBytesRemaining(12345L)
+                .setExpiryTime(1000000L)
+                .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_NA_PASSPOINT),
+                        CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT)
+                .setUserPortalUrl(Uri.parse(TEST_TERMS_AND_CONDITIONS_URL_NA_PASSPOINT),
+                        CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT)
+                .setVenueFriendlyName(TEST_FRIENDLY_NAME).build();
+
+        final CaptivePortalData expectedMergedOtherData = new CaptivePortalData.Builder()
+                .setUserPortalUrl(Uri.parse(TEST_REDIRECT_URL))
+                .setBytesRemaining(12345L)
+                .setExpiryTime(1000000L)
+                .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_CAPPORT))
+                .setUserPortalUrl(Uri.parse(TEST_USER_PORTAL_API_URL_CAPPORT))
+                .setVenueFriendlyName(TEST_FRIENDLY_NAME).build();
+        return new CaptivePortalTestData(naPasspointData, capportData, naOtherData,
+                expectedMergedPasspointData, expectedMergedOtherData);
+    }
+
+    @Test
+    public void testMergeCaptivePortalApiWithFriendlyNameAndVenueUrl() throws Exception {
+        final TestNetworkCallback captivePortalCallback = setupNetworkCallbackAndConnectToWifi();
+        final CaptivePortalTestData captivePortalTestData = setupCaptivePortalData();
+
+        // Baseline capport data
+        mWiFiNetworkAgent.notifyCapportApiDataChanged(captivePortalTestData.mCapportData);
+
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> captivePortalTestData.mCapportData.equals(lp.getCaptivePortalData()));
+
+        // Venue URL, T&C URL and friendly name from Network agent with Passpoint source, confirm
+        // that API data gets precedence on the bytes remaining.
+        final LinkProperties linkProperties = new LinkProperties();
+        linkProperties.setCaptivePortalData(captivePortalTestData.mNaPasspointData);
+        mWiFiNetworkAgent.sendLinkProperties(linkProperties);
+
+        // Make sure that the capport data is merged
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> captivePortalTestData.mExpectedMergedPasspointData
+                        .equals(lp.getCaptivePortalData()));
+
+        // Now send this information from non-Passpoint source, confirm that Capport data takes
+        // precedence
+        linkProperties.setCaptivePortalData(captivePortalTestData.mNaOtherData);
+        mWiFiNetworkAgent.sendLinkProperties(linkProperties);
+
+        // Make sure that the capport data is merged
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> captivePortalTestData.mExpectedMergedOtherData
+                        .equals(lp.getCaptivePortalData()));
+
+        // Create a new LP with no Network agent capport data
+        final LinkProperties newLps = new LinkProperties();
+        newLps.setMtu(1234);
+        mWiFiNetworkAgent.sendLinkProperties(newLps);
+        // CaptivePortalData is not lost and has the original values when LPs are received from the
+        // NetworkAgent
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> captivePortalTestData.mCapportData.equals(lp.getCaptivePortalData())
+                        && lp.getMtu() == 1234);
+
+        // Now send capport data only from the Network agent
+        mWiFiNetworkAgent.notifyCapportApiDataChanged(null);
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> lp.getCaptivePortalData() == null);
+
+        newLps.setCaptivePortalData(captivePortalTestData.mNaPasspointData);
+        mWiFiNetworkAgent.sendLinkProperties(newLps);
+
+        // Make sure that only the network agent capport data is available
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> captivePortalTestData.mNaPasspointData.equals(lp.getCaptivePortalData()));
+    }
+
+    @Test
+    public void testMergeCaptivePortalDataFromNetworkAgentFirstThenCapport() throws Exception {
+        final TestNetworkCallback captivePortalCallback = setupNetworkCallbackAndConnectToWifi();
+        final CaptivePortalTestData captivePortalTestData = setupCaptivePortalData();
+
+        // Venue URL and friendly name from Network agent, confirm that API data gets precedence
+        // on the bytes remaining.
+        final LinkProperties linkProperties = new LinkProperties();
+        linkProperties.setCaptivePortalData(captivePortalTestData.mNaPasspointData);
+        mWiFiNetworkAgent.sendLinkProperties(linkProperties);
+
+        // Make sure that the data is saved correctly
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> captivePortalTestData.mNaPasspointData.equals(lp.getCaptivePortalData()));
+
+        // Expected merged data: Network agent data is preferred, and values that are not used by
+        // it are merged from capport data
+        mWiFiNetworkAgent.notifyCapportApiDataChanged(captivePortalTestData.mCapportData);
+
+        // Make sure that the Capport data is merged correctly
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> captivePortalTestData.mExpectedMergedPasspointData.equals(
+                        lp.getCaptivePortalData()));
+
+        // Now set the naData to null
+        linkProperties.setCaptivePortalData(null);
+        mWiFiNetworkAgent.sendLinkProperties(linkProperties);
+
+        // Make sure that the Capport data is retained correctly
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> captivePortalTestData.mCapportData.equals(lp.getCaptivePortalData()));
+    }
+
+    @Test
+    public void testMergeCaptivePortalDataFromNetworkAgentOtherSourceFirstThenCapport()
+            throws Exception {
+        final TestNetworkCallback captivePortalCallback = setupNetworkCallbackAndConnectToWifi();
+        final CaptivePortalTestData captivePortalTestData = setupCaptivePortalData();
+
+        // Venue URL and friendly name from Network agent, confirm that API data gets precedence
+        // on the bytes remaining.
+        final LinkProperties linkProperties = new LinkProperties();
+        linkProperties.setCaptivePortalData(captivePortalTestData.mNaOtherData);
+        mWiFiNetworkAgent.sendLinkProperties(linkProperties);
+
+        // Make sure that the data is saved correctly
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> captivePortalTestData.mNaOtherData.equals(lp.getCaptivePortalData()));
+
+        // Expected merged data: Network agent data is preferred, and values that are not used by
+        // it are merged from capport data
+        mWiFiNetworkAgent.notifyCapportApiDataChanged(captivePortalTestData.mCapportData);
+
+        // Make sure that the Capport data is merged correctly
+        captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent,
+                lp -> captivePortalTestData.mExpectedMergedOtherData.equals(
+                        lp.getCaptivePortalData()));
+    }
+
+    private NetworkRequest.Builder newWifiRequestBuilder() {
+        return new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI);
+    }
+
+    /**
+     * Verify request matching behavior with network specifiers.
+     *
+     * This test does not check updating the specifier on a live network because the specifier is
+     * immutable and this triggers a WTF in
+     * {@link ConnectivityService#mixInCapabilities(NetworkAgentInfo, NetworkCapabilities)}.
+     */
+    @Test
+    public void testNetworkSpecifier() throws Exception {
+        // A NetworkSpecifier subclass that matches all networks but must not be visible to apps.
+        class ConfidentialMatchAllNetworkSpecifier extends NetworkSpecifier implements
+                Parcelable {
+            @Override
+            public boolean canBeSatisfiedBy(NetworkSpecifier other) {
+                return true;
+            }
+
+            @Override
+            public int describeContents() {
+                return 0;
+            }
+
+            @Override
+            public void writeToParcel(Parcel dest, int flags) {}
+
+            @Override
+            public NetworkSpecifier redact() {
+                return null;
+            }
+        }
+
+        // A network specifier that matches either another LocalNetworkSpecifier with the same
+        // string or a ConfidentialMatchAllNetworkSpecifier, and can be passed to apps as is.
+        class LocalStringNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+            private String mString;
+
+            LocalStringNetworkSpecifier(String string) {
+                mString = string;
+            }
+
+            @Override
+            public boolean canBeSatisfiedBy(NetworkSpecifier other) {
+                if (other instanceof LocalStringNetworkSpecifier) {
+                    return TextUtils.equals(mString,
+                            ((LocalStringNetworkSpecifier) other).mString);
+                }
+                if (other instanceof ConfidentialMatchAllNetworkSpecifier) return true;
+                return false;
+            }
+
+            @Override
+            public int describeContents() {
+                return 0;
+            }
+            @Override
+            public void writeToParcel(Parcel dest, int flags) {}
+        }
+
+
+        NetworkRequest rEmpty1 = newWifiRequestBuilder().build();
+        NetworkRequest rEmpty2 = newWifiRequestBuilder().setNetworkSpecifier((String) null).build();
+        NetworkRequest rEmpty3 = newWifiRequestBuilder().setNetworkSpecifier("").build();
+        NetworkRequest rEmpty4 = newWifiRequestBuilder().setNetworkSpecifier(
+            (NetworkSpecifier) null).build();
+        NetworkRequest rFoo = newWifiRequestBuilder().setNetworkSpecifier(
+                new LocalStringNetworkSpecifier("foo")).build();
+        NetworkRequest rBar = newWifiRequestBuilder().setNetworkSpecifier(
+                new LocalStringNetworkSpecifier("bar")).build();
+
+        TestNetworkCallback cEmpty1 = new TestNetworkCallback();
+        TestNetworkCallback cEmpty2 = new TestNetworkCallback();
+        TestNetworkCallback cEmpty3 = new TestNetworkCallback();
+        TestNetworkCallback cEmpty4 = new TestNetworkCallback();
+        TestNetworkCallback cFoo = new TestNetworkCallback();
+        TestNetworkCallback cBar = new TestNetworkCallback();
+        TestNetworkCallback[] emptyCallbacks = new TestNetworkCallback[] {
+                cEmpty1, cEmpty2, cEmpty3, cEmpty4 };
+
+        mCm.registerNetworkCallback(rEmpty1, cEmpty1);
+        mCm.registerNetworkCallback(rEmpty2, cEmpty2);
+        mCm.registerNetworkCallback(rEmpty3, cEmpty3);
+        mCm.registerNetworkCallback(rEmpty4, cEmpty4);
+        mCm.registerNetworkCallback(rFoo, cFoo);
+        mCm.registerNetworkCallback(rBar, cBar);
+
+        LocalStringNetworkSpecifier nsFoo = new LocalStringNetworkSpecifier("foo");
+        LocalStringNetworkSpecifier nsBar = new LocalStringNetworkSpecifier("bar");
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        expectAvailableCallbacksUnvalidatedWithSpecifier(mWiFiNetworkAgent, null /* specifier */,
+                cEmpty1, cEmpty2, cEmpty3, cEmpty4);
+        assertNoCallbacks(cFoo, cBar);
+
+        mWiFiNetworkAgent.disconnect();
+        expectOnLost(mWiFiNetworkAgent, cEmpty1, cEmpty2, cEmpty3, cEmpty4);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.setNetworkSpecifier(nsFoo);
+        mWiFiNetworkAgent.connect(false);
+        expectAvailableCallbacksUnvalidatedWithSpecifier(mWiFiNetworkAgent, nsFoo,
+                cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo);
+        cBar.assertNoCallback();
+        assertEquals(nsFoo,
+                mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).getNetworkSpecifier());
+        assertNoCallbacks(cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo);
+
+        mWiFiNetworkAgent.disconnect();
+        expectOnLost(mWiFiNetworkAgent, cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.setNetworkSpecifier(nsBar);
+        mWiFiNetworkAgent.connect(false);
+        expectAvailableCallbacksUnvalidatedWithSpecifier(mWiFiNetworkAgent, nsBar,
+                cEmpty1, cEmpty2, cEmpty3, cEmpty4, cBar);
+        cFoo.assertNoCallback();
+        assertEquals(nsBar,
+                mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).getNetworkSpecifier());
+
+        mWiFiNetworkAgent.disconnect();
+        expectOnLost(mWiFiNetworkAgent, cEmpty1, cEmpty2, cEmpty3, cEmpty4, cBar);
+        cFoo.assertNoCallback();
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.setNetworkSpecifier(new ConfidentialMatchAllNetworkSpecifier());
+        mWiFiNetworkAgent.connect(false);
+        expectAvailableCallbacksUnvalidatedWithSpecifier(mWiFiNetworkAgent, null /* specifier */,
+                cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo, cBar);
+        assertNull(
+                mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).getNetworkSpecifier());
+
+        mWiFiNetworkAgent.disconnect();
+        expectOnLost(mWiFiNetworkAgent, cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo, cBar);
+    }
+
+    /**
+     * @return the context's attribution tag
+     */
+    private String getAttributionTag() {
+        return null;
+    }
+
+    @Test
+    public void testInvalidNetworkSpecifier() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            NetworkRequest.Builder builder = new NetworkRequest.Builder();
+            builder.setNetworkSpecifier(new MatchAllNetworkSpecifier());
+        });
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            NetworkCapabilities networkCapabilities = new NetworkCapabilities();
+            networkCapabilities.addTransportType(TRANSPORT_WIFI)
+                    .setNetworkSpecifier(new MatchAllNetworkSpecifier());
+            mService.requestNetwork(Process.INVALID_UID, networkCapabilities,
+                    NetworkRequest.Type.REQUEST.ordinal(), null, 0, null,
+                    ConnectivityManager.TYPE_WIFI, NetworkCallback.FLAG_NONE,
+                    mContext.getPackageName(), getAttributionTag());
+        });
+
+        class NonParcelableSpecifier extends NetworkSpecifier {
+            @Override
+            public boolean canBeSatisfiedBy(NetworkSpecifier other) {
+                return false;
+            }
+        };
+        class ParcelableSpecifier extends NonParcelableSpecifier implements Parcelable {
+            @Override public int describeContents() { return 0; }
+            @Override public void writeToParcel(Parcel p, int flags) {}
+        }
+
+        final NetworkRequest.Builder builder =
+                new NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET);
+        assertThrows(ClassCastException.class, () -> {
+            builder.setNetworkSpecifier(new NonParcelableSpecifier());
+            Parcel parcelW = Parcel.obtain();
+            builder.build().writeToParcel(parcelW, 0);
+        });
+
+        final NetworkRequest nr =
+                new NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET)
+                .setNetworkSpecifier(new ParcelableSpecifier())
+                .build();
+        assertNotNull(nr);
+
+        assertThrows(BadParcelableException.class, () -> {
+            Parcel parcelW = Parcel.obtain();
+            nr.writeToParcel(parcelW, 0);
+            byte[] bytes = parcelW.marshall();
+            parcelW.recycle();
+
+            Parcel parcelR = Parcel.obtain();
+            parcelR.unmarshall(bytes, 0, bytes.length);
+            parcelR.setDataPosition(0);
+            NetworkRequest rereadNr = NetworkRequest.CREATOR.createFromParcel(parcelR);
+        });
+    }
+
+    @Test
+    public void testNetworkRequestUidSpoofSecurityException() throws Exception {
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        NetworkRequest networkRequest = newWifiRequestBuilder().build();
+        TestNetworkCallback networkCallback = new TestNetworkCallback();
+        doThrow(new SecurityException()).when(mAppOpsManager).checkPackage(anyInt(), anyString());
+        assertThrows(SecurityException.class, () -> {
+            mCm.requestNetwork(networkRequest, networkCallback);
+        });
+    }
+
+    @Test
+    public void testInvalidSignalStrength() {
+        NetworkRequest r = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addTransportType(TRANSPORT_WIFI)
+                .setSignalStrength(-75)
+                .build();
+        // Registering a NetworkCallback with signal strength but w/o NETWORK_SIGNAL_STRENGTH_WAKEUP
+        // permission should get SecurityException.
+        assertThrows(SecurityException.class, () ->
+                mCm.registerNetworkCallback(r, new NetworkCallback()));
+
+        assertThrows(SecurityException.class, () ->
+                mCm.registerNetworkCallback(r, PendingIntent.getService(
+                        mServiceContext, 0 /* requestCode */, new Intent(), FLAG_IMMUTABLE)));
+
+        // Requesting a Network with signal strength should get IllegalArgumentException.
+        assertThrows(IllegalArgumentException.class, () ->
+                mCm.requestNetwork(r, new NetworkCallback()));
+
+        assertThrows(IllegalArgumentException.class, () ->
+                mCm.requestNetwork(r, PendingIntent.getService(
+                        mServiceContext, 0 /* requestCode */, new Intent(), FLAG_IMMUTABLE)));
+    }
+
+    @Test
+    public void testRegisterDefaultNetworkCallback() throws Exception {
+        // NETWORK_SETTINGS is necessary to call registerSystemDefaultNetworkCallback.
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+        final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultNetworkCallback);
+        defaultNetworkCallback.assertNoCallback();
+
+        final Handler handler = new Handler(ConnectivityThread.getInstanceLooper());
+        final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback();
+        mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback, handler);
+        systemDefaultCallback.assertNoCallback();
+
+        // Create a TRANSPORT_CELLULAR request to keep the mobile interface up
+        // whenever Wi-Fi is up. Without this, the mobile network agent is
+        // reaped before any other activity can take place.
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.requestNetwork(cellRequest, cellNetworkCallback);
+        cellNetworkCallback.assertNoCallback();
+
+        // Bring up cell and expect CALLBACK_AVAILABLE.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        systemDefaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+        assertEquals(systemDefaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        // Bring up wifi and expect CALLBACK_AVAILABLE.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        cellNetworkCallback.assertNoCallback();
+        defaultNetworkCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        systemDefaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+        assertEquals(systemDefaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        // Bring down cell. Expect no default network callback, since it wasn't the default.
+        mCellNetworkAgent.disconnect();
+        cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        defaultNetworkCallback.assertNoCallback();
+        systemDefaultCallback.assertNoCallback();
+        assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+        assertEquals(systemDefaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        // Bring up cell. Expect no default network callback, since it won't be the default.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        defaultNetworkCallback.assertNoCallback();
+        systemDefaultCallback.assertNoCallback();
+        assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+        assertEquals(systemDefaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        // Bring down wifi. Expect the default network callback to notified of LOST wifi
+        // followed by AVAILABLE cell.
+        mWiFiNetworkAgent.disconnect();
+        cellNetworkCallback.assertNoCallback();
+        defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        defaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        systemDefaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        systemDefaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        mCellNetworkAgent.disconnect();
+        cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        systemDefaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        waitForIdle();
+        assertEquals(null, mCm.getActiveNetwork());
+
+        mMockVpn.establishForMyUid();
+        assertUidRangesUpdatedForMyUid(true);
+        defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        systemDefaultCallback.assertNoCallback();
+        assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+        assertEquals(null, systemDefaultCallback.getLastAvailableNetwork());
+
+        mMockVpn.disconnect();
+        defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn);
+        systemDefaultCallback.assertNoCallback();
+        waitForIdle();
+        assertEquals(null, mCm.getActiveNetwork());
+    }
+
+    @Test
+    public void testAdditionalStateCallbacks() throws Exception {
+        // File a network request for mobile.
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.requestNetwork(cellRequest, cellNetworkCallback);
+
+        // Bring up the mobile network.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        // We should get onAvailable(), onCapabilitiesChanged(), and
+        // onLinkPropertiesChanged() in rapid succession. Additionally, we
+        // should get onCapabilitiesChanged() when the mobile network validates.
+        cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+
+        // Update LinkProperties.
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("foonet_data0");
+        mCellNetworkAgent.sendLinkProperties(lp);
+        // We should get onLinkPropertiesChanged().
+        cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED,
+                mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+
+        // Suspend the network.
+        mCellNetworkAgent.suspend();
+        cellNetworkCallback.expectCapabilitiesWithout(NET_CAPABILITY_NOT_SUSPENDED,
+                mCellNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackEntry.SUSPENDED, mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        assertEquals(NetworkInfo.State.SUSPENDED, mCm.getActiveNetworkInfo().getState());
+
+        // Register a garden variety default network request.
+        TestNetworkCallback dfltNetworkCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(dfltNetworkCallback);
+        // We should get onAvailable(), onCapabilitiesChanged(), onLinkPropertiesChanged(),
+        // as well as onNetworkSuspended() in rapid succession.
+        dfltNetworkCallback.expectAvailableAndSuspendedCallbacks(mCellNetworkAgent, true);
+        dfltNetworkCallback.assertNoCallback();
+        mCm.unregisterNetworkCallback(dfltNetworkCallback);
+
+        mCellNetworkAgent.resume();
+        cellNetworkCallback.expectCapabilitiesWith(NET_CAPABILITY_NOT_SUSPENDED,
+                mCellNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackEntry.RESUMED, mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        assertEquals(NetworkInfo.State.CONNECTED, mCm.getActiveNetworkInfo().getState());
+
+        dfltNetworkCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(dfltNetworkCallback);
+        // This time onNetworkSuspended should not be called.
+        dfltNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        dfltNetworkCallback.assertNoCallback();
+
+        mCm.unregisterNetworkCallback(dfltNetworkCallback);
+        mCm.unregisterNetworkCallback(cellNetworkCallback);
+    }
+
+    @Test
+    public void testRegisterPrivilegedDefaultCallbacksRequireNetworkSettings() throws Exception {
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(false /* validated */);
+
+        final Handler handler = new Handler(ConnectivityThread.getInstanceLooper());
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        assertThrows(SecurityException.class,
+                () -> mCm.registerSystemDefaultNetworkCallback(callback, handler));
+        callback.assertNoCallback();
+        assertThrows(SecurityException.class,
+                () -> mCm.registerDefaultNetworkCallbackForUid(APP1_UID, callback, handler));
+        callback.assertNoCallback();
+
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+        mCm.registerSystemDefaultNetworkCallback(callback, handler);
+        callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        mCm.unregisterNetworkCallback(callback);
+
+        mCm.registerDefaultNetworkCallbackForUid(APP1_UID, callback, handler);
+        callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        mCm.unregisterNetworkCallback(callback);
+    }
+
+    private void setCaptivePortalMode(int mode) {
+        ContentResolver cr = mServiceContext.getContentResolver();
+        Settings.Global.putInt(cr, ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE, mode);
+    }
+
+    private void setAlwaysOnNetworks(boolean enable) {
+        ContentResolver cr = mServiceContext.getContentResolver();
+        Settings.Global.putInt(cr, ConnectivitySettingsManager.MOBILE_DATA_ALWAYS_ON,
+                enable ? 1 : 0);
+        mService.updateAlwaysOnNetworks();
+        waitForIdle();
+    }
+
+    private void setPrivateDnsSettings(int mode, String specifier) {
+        ConnectivitySettingsManager.setPrivateDnsMode(mServiceContext, mode);
+        ConnectivitySettingsManager.setPrivateDnsHostname(mServiceContext, specifier);
+        mService.updatePrivateDnsSettings();
+        waitForIdle();
+    }
+
+    private boolean isForegroundNetwork(TestNetworkAgentWrapper network) {
+        NetworkCapabilities nc = mCm.getNetworkCapabilities(network.getNetwork());
+        assertNotNull(nc);
+        return nc.hasCapability(NET_CAPABILITY_FOREGROUND);
+    }
+
+    @Test
+    public void testBackgroundNetworks() throws Exception {
+        // Create a cellular background request.
+        grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid());
+        final TestNetworkCallback cellBgCallback = new TestNetworkCallback();
+        mCm.requestBackgroundNetwork(new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build(),
+                cellBgCallback, mCsHandlerThread.getThreadHandler());
+
+        // Make callbacks for monitoring.
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        final NetworkRequest fgRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_FOREGROUND).build();
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        final TestNetworkCallback fgCallback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+        mCm.registerNetworkCallback(fgRequest, fgCallback);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        fgCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        assertTrue(isForegroundNetwork(mCellNetworkAgent));
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+
+        // When wifi connects, cell lingers.
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        fgCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        fgCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        fgCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        assertTrue(isForegroundNetwork(mCellNetworkAgent));
+        assertTrue(isForegroundNetwork(mWiFiNetworkAgent));
+
+        // When lingering is complete, cell is still there but is now in the background.
+        waitForIdle();
+        int timeoutMs = TEST_LINGER_DELAY_MS + TEST_LINGER_DELAY_MS / 4;
+        fgCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent, timeoutMs);
+        // Expect a network capabilities update sans FOREGROUND.
+        callback.expectCapabilitiesWithout(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent);
+        assertFalse(isForegroundNetwork(mCellNetworkAgent));
+        assertTrue(isForegroundNetwork(mWiFiNetworkAgent));
+
+        // File a cell request and check that cell comes into the foreground.
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        final TestNetworkCallback cellCallback = new TestNetworkCallback();
+        mCm.requestNetwork(cellRequest, cellCallback);
+        cellCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        fgCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        // Expect a network capabilities update with FOREGROUND, because the most recent
+        // request causes its state to change.
+        cellCallback.expectCapabilitiesWith(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent);
+        callback.expectCapabilitiesWith(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent);
+        assertTrue(isForegroundNetwork(mCellNetworkAgent));
+        assertTrue(isForegroundNetwork(mWiFiNetworkAgent));
+
+        // Release the request. The network immediately goes into the background, since it was not
+        // lingering.
+        mCm.unregisterNetworkCallback(cellCallback);
+        fgCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        // Expect a network capabilities update sans FOREGROUND.
+        callback.expectCapabilitiesWithout(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent);
+        assertFalse(isForegroundNetwork(mCellNetworkAgent));
+        assertTrue(isForegroundNetwork(mWiFiNetworkAgent));
+
+        // Disconnect wifi and check that cell is foreground again.
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        fgCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        fgCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        assertTrue(isForegroundNetwork(mCellNetworkAgent));
+
+        mCm.unregisterNetworkCallback(callback);
+        mCm.unregisterNetworkCallback(fgCallback);
+        mCm.unregisterNetworkCallback(cellBgCallback);
+    }
+
+    @Ignore // This test has instrinsic chances of spurious failures: ignore for continuous testing.
+    public void benchmarkRequestRegistrationAndCallbackDispatch() throws Exception {
+        // TODO: turn this unit test into a real benchmarking test.
+        // Benchmarks connecting and switching performance in the presence of a large number of
+        // NetworkRequests.
+        // 1. File NUM_REQUESTS requests.
+        // 2. Have a network connect. Wait for NUM_REQUESTS onAvailable callbacks to fire.
+        // 3. Have a new network connect and outscore the previous. Wait for NUM_REQUESTS onLosing
+        //    and NUM_REQUESTS onAvailable callbacks to fire.
+        // See how long it took.
+        final int NUM_REQUESTS = 90;
+        final int REGISTER_TIME_LIMIT_MS = 200;
+        final int CONNECT_TIME_LIMIT_MS = 60;
+        final int SWITCH_TIME_LIMIT_MS = 60;
+        final int UNREGISTER_TIME_LIMIT_MS = 20;
+
+        final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
+        final NetworkCallback[] callbacks = new NetworkCallback[NUM_REQUESTS];
+        final CountDownLatch availableLatch = new CountDownLatch(NUM_REQUESTS);
+        final CountDownLatch losingLatch = new CountDownLatch(NUM_REQUESTS);
+
+        for (int i = 0; i < NUM_REQUESTS; i++) {
+            callbacks[i] = new NetworkCallback() {
+                @Override public void onAvailable(Network n) { availableLatch.countDown(); }
+                @Override public void onLosing(Network n, int t) { losingLatch.countDown(); }
+            };
+        }
+
+        assertRunsInAtMost("Registering callbacks", REGISTER_TIME_LIMIT_MS, () -> {
+            for (NetworkCallback cb : callbacks) {
+                mCm.registerNetworkCallback(request, cb);
+            }
+        });
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        // Don't request that the network validate, because otherwise connect() will block until
+        // the network gets NET_CAPABILITY_VALIDATED, after all the callbacks below have fired,
+        // and we won't actually measure anything.
+        mCellNetworkAgent.connect(false);
+
+        long onAvailableDispatchingDuration = durationOf(() -> {
+            await(availableLatch, 10 * CONNECT_TIME_LIMIT_MS);
+        });
+        Log.d(TAG, String.format("Dispatched %d of %d onAvailable callbacks in %dms",
+                NUM_REQUESTS - availableLatch.getCount(), NUM_REQUESTS,
+                onAvailableDispatchingDuration));
+        assertTrue(String.format("Dispatching %d onAvailable callbacks in %dms, expected %dms",
+                NUM_REQUESTS, onAvailableDispatchingDuration, CONNECT_TIME_LIMIT_MS),
+                onAvailableDispatchingDuration <= CONNECT_TIME_LIMIT_MS);
+
+        // Give wifi a high enough score that we'll linger cell when wifi comes up.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.adjustScore(40);
+        mWiFiNetworkAgent.connect(false);
+
+        long onLostDispatchingDuration = durationOf(() -> {
+            await(losingLatch, 10 * SWITCH_TIME_LIMIT_MS);
+        });
+        Log.d(TAG, String.format("Dispatched %d of %d onLosing callbacks in %dms",
+                NUM_REQUESTS - losingLatch.getCount(), NUM_REQUESTS, onLostDispatchingDuration));
+        assertTrue(String.format("Dispatching %d onLosing callbacks in %dms, expected %dms",
+                NUM_REQUESTS, onLostDispatchingDuration, SWITCH_TIME_LIMIT_MS),
+                onLostDispatchingDuration <= SWITCH_TIME_LIMIT_MS);
+
+        assertRunsInAtMost("Unregistering callbacks", UNREGISTER_TIME_LIMIT_MS, () -> {
+            for (NetworkCallback cb : callbacks) {
+                mCm.unregisterNetworkCallback(cb);
+            }
+        });
+    }
+
+    @Test
+    public void testMobileDataAlwaysOn() throws Exception {
+        grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid());
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.registerNetworkCallback(cellRequest, cellNetworkCallback);
+
+        final HandlerThread handlerThread = new HandlerThread("MobileDataAlwaysOnFactory");
+        handlerThread.start();
+        NetworkCapabilities filter = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .addCapability(NET_CAPABILITY_INTERNET);
+        final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "testFactory", filter, mCsHandlerThread);
+        testFactory.setScoreFilter(40);
+
+        // Register the factory and expect it to start looking for a network.
+        testFactory.register();
+
+        try {
+            // Expect the factory to receive the default network request.
+            testFactory.expectRequestAdd();
+            testFactory.assertRequestCountEquals(1);
+            assertTrue(testFactory.getMyStartRequested());
+
+            // Bring up wifi. The factory stops looking for a network.
+            mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+            // Score 60 - 40 penalty for not validated yet, then 60 when it validates
+            mWiFiNetworkAgent.connect(true);
+            // The network connects with a low score, so the offer can still beat it and
+            // nothing happens. Then the network validates, and the offer with its filter score
+            // of 40 can no longer beat it and the request is removed.
+            testFactory.expectRequestRemove();
+            testFactory.assertRequestCountEquals(0);
+
+            assertFalse(testFactory.getMyStartRequested());
+
+            // Turn on mobile data always on. This request will not match the wifi request, so
+            // it will be sent to the test factory whose filters allow to see it.
+            setAlwaysOnNetworks(true);
+            testFactory.expectRequestAdd();
+            testFactory.assertRequestCountEquals(1);
+
+            assertTrue(testFactory.getMyStartRequested());
+
+            // Bring up cell data and check that the factory stops looking.
+            assertLength(1, mCm.getAllNetworks());
+            mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+            mCellNetworkAgent.connect(false);
+            cellNetworkCallback.expectAvailableCallbacks(mCellNetworkAgent, false, false, false,
+                    TEST_CALLBACK_TIMEOUT_MS);
+            // When cell connects, it will satisfy the "mobile always on request" right away
+            // by virtue of being the only network that can satisfy the request. However, its
+            // score is low (50 - 40 = 10) so the test factory can still hope to beat it.
+            expectNoRequestChanged(testFactory);
+
+            // Next, cell validates. This gives it a score of 50 and the test factory can't
+            // hope to beat that according to its filters. It will see the message that its
+            // offer is now unnecessary.
+            mCellNetworkAgent.setNetworkValid(true);
+            // Need a trigger point to let NetworkMonitor tell ConnectivityService that network is
+            // validated – see testPartialConnectivity.
+            mCm.reportNetworkConnectivity(mCellNetworkAgent.getNetwork(), true);
+            cellNetworkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mCellNetworkAgent);
+            testFactory.expectRequestRemove();
+            testFactory.assertRequestCountEquals(0);
+            // Accordingly, the factory shouldn't be started.
+            assertFalse(testFactory.getMyStartRequested());
+
+            // Check that cell data stays up.
+            waitForIdle();
+            verifyActiveNetwork(TRANSPORT_WIFI);
+            assertLength(2, mCm.getAllNetworks());
+
+            // Cell disconnects. There is still the "mobile data always on" request outstanding,
+            // and the test factory should see it now that it isn't hopelessly outscored.
+            mCellNetworkAgent.disconnect();
+            cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+            assertLength(1, mCm.getAllNetworks());
+            testFactory.expectRequestAdd();
+            testFactory.assertRequestCountEquals(1);
+
+            // Reconnect cell validated, see the request disappear again. Then withdraw the
+            // mobile always on request. This will tear down cell, and there shouldn't be a
+            // blip where the test factory briefly sees the request or anything.
+            mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+            mCellNetworkAgent.connect(true);
+            cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+            assertLength(2, mCm.getAllNetworks());
+            testFactory.expectRequestRemove();
+            testFactory.assertRequestCountEquals(0);
+            setAlwaysOnNetworks(false);
+            expectNoRequestChanged(testFactory);
+            testFactory.assertRequestCountEquals(0);
+            assertFalse(testFactory.getMyStartRequested());
+            // ...  and cell data to be torn down immediately since it is no longer nascent.
+            cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+            waitForIdle();
+            assertLength(1, mCm.getAllNetworks());
+        } finally {
+            testFactory.terminate();
+            mCm.unregisterNetworkCallback(cellNetworkCallback);
+            handlerThread.quit();
+        }
+    }
+
+    @Test
+    public void testAvoidBadWifiSetting() throws Exception {
+        final ContentResolver cr = mServiceContext.getContentResolver();
+        final String settingName = ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI;
+
+        mPolicyTracker.mConfigRestrictsAvoidBadWifi = false;
+        String[] values = new String[] {null, "0", "1"};
+        for (int i = 0; i < values.length; i++) {
+            Settings.Global.putInt(cr, settingName, 1);
+            mPolicyTracker.reevaluate();
+            waitForIdle();
+            String msg = String.format("config=false, setting=%s", values[i]);
+            assertTrue(mService.avoidBadWifi());
+            assertFalse(msg, mPolicyTracker.shouldNotifyWifiUnvalidated());
+        }
+
+        mPolicyTracker.mConfigRestrictsAvoidBadWifi = true;
+
+        Settings.Global.putInt(cr, settingName, 0);
+        mPolicyTracker.reevaluate();
+        waitForIdle();
+        assertFalse(mService.avoidBadWifi());
+        assertFalse(mPolicyTracker.shouldNotifyWifiUnvalidated());
+
+        Settings.Global.putInt(cr, settingName, 1);
+        mPolicyTracker.reevaluate();
+        waitForIdle();
+        assertTrue(mService.avoidBadWifi());
+        assertFalse(mPolicyTracker.shouldNotifyWifiUnvalidated());
+
+        Settings.Global.putString(cr, settingName, null);
+        mPolicyTracker.reevaluate();
+        waitForIdle();
+        assertFalse(mService.avoidBadWifi());
+        assertTrue(mPolicyTracker.shouldNotifyWifiUnvalidated());
+    }
+
+    @Ignore("Refactoring in progress b/178071397")
+    @Test
+    public void testAvoidBadWifi() throws Exception {
+        final ContentResolver cr = mServiceContext.getContentResolver();
+
+        // Pretend we're on a carrier that restricts switching away from bad wifi.
+        mPolicyTracker.mConfigRestrictsAvoidBadWifi = true;
+
+        // File a request for cell to ensure it doesn't go down.
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.requestNetwork(cellRequest, cellNetworkCallback);
+
+        TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+
+        NetworkRequest validatedWifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_VALIDATED)
+                .build();
+        TestNetworkCallback validatedWifiCallback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(validatedWifiRequest, validatedWifiCallback);
+
+        Settings.Global.putInt(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, 0);
+        mPolicyTracker.reevaluate();
+
+        // Bring up validated cell.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        Network cellNetwork = mCellNetworkAgent.getNetwork();
+
+        // Bring up validated wifi.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        validatedWifiCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        Network wifiNetwork = mWiFiNetworkAgent.getNetwork();
+
+        // Fail validation on wifi.
+        mWiFiNetworkAgent.setNetworkInvalid(false /* isStrictMode */);
+        mCm.reportNetworkConnectivity(wifiNetwork, false);
+        defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        validatedWifiCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        // Because avoid bad wifi is off, we don't switch to cellular.
+        defaultCallback.assertNoCallback();
+        assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+
+        // Simulate switching to a carrier that does not restrict avoiding bad wifi, and expect
+        // that we switch back to cell.
+        mPolicyTracker.mConfigRestrictsAvoidBadWifi = false;
+        mPolicyTracker.reevaluate();
+        defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+        // Switch back to a restrictive carrier.
+        mPolicyTracker.mConfigRestrictsAvoidBadWifi = true;
+        mPolicyTracker.reevaluate();
+        defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+
+        // Simulate the user selecting "switch" on the dialog, and check that we switch to cell.
+        mCm.setAvoidUnvalidated(wifiNetwork);
+        defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+        // Disconnect and reconnect wifi to clear the one-time switch above.
+        mWiFiNetworkAgent.disconnect();
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        validatedWifiCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        wifiNetwork = mWiFiNetworkAgent.getNetwork();
+
+        // Fail validation on wifi and expect the dialog to appear.
+        mWiFiNetworkAgent.setNetworkInvalid(false /* isStrictMode */);
+        mCm.reportNetworkConnectivity(wifiNetwork, false);
+        defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        validatedWifiCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        // Simulate the user selecting "switch" and checking the don't ask again checkbox.
+        Settings.Global.putInt(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, 1);
+        mPolicyTracker.reevaluate();
+
+        // We now switch to cell.
+        defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+        // Simulate the user turning the cellular fallback setting off and then on.
+        // We switch to wifi and then to cell.
+        Settings.Global.putString(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, null);
+        mPolicyTracker.reevaluate();
+        defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+        Settings.Global.putInt(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, 1);
+        mPolicyTracker.reevaluate();
+        defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+        // If cell goes down, we switch to wifi.
+        mCellNetworkAgent.disconnect();
+        defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        validatedWifiCallback.assertNoCallback();
+
+        mCm.unregisterNetworkCallback(cellNetworkCallback);
+        mCm.unregisterNetworkCallback(validatedWifiCallback);
+        mCm.unregisterNetworkCallback(defaultCallback);
+    }
+
+    @Test
+    public void testMeteredMultipathPreferenceSetting() throws Exception {
+        final ContentResolver cr = mServiceContext.getContentResolver();
+        final String settingName = ConnectivitySettingsManager.NETWORK_METERED_MULTIPATH_PREFERENCE;
+
+        for (int config : Arrays.asList(0, 3, 2)) {
+            for (String setting: Arrays.asList(null, "0", "2", "1")) {
+                mPolicyTracker.mConfigMeteredMultipathPreference = config;
+                Settings.Global.putString(cr, settingName, setting);
+                mPolicyTracker.reevaluate();
+                waitForIdle();
+
+                final int expected = (setting != null) ? Integer.parseInt(setting) : config;
+                String msg = String.format("config=%d, setting=%s", config, setting);
+                assertEquals(msg, expected, mCm.getMultipathPreference(null));
+            }
+        }
+    }
+
+    /**
+     * Validate that a satisfied network request does not trigger onUnavailable() once the
+     * time-out period expires.
+     */
+    @Test
+    public void testSatisfiedNetworkRequestDoesNotTriggerOnUnavailable() throws Exception {
+        NetworkRequest nr = new NetworkRequest.Builder().addTransportType(
+                NetworkCapabilities.TRANSPORT_WIFI).build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.requestNetwork(nr, networkCallback, TEST_REQUEST_TIMEOUT_MS);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        networkCallback.expectAvailableCallbacks(mWiFiNetworkAgent, false, false, false,
+                TEST_CALLBACK_TIMEOUT_MS);
+
+        // pass timeout and validate that UNAVAILABLE is not called
+        networkCallback.assertNoCallback();
+    }
+
+    /**
+     * Validate that a satisfied network request followed by a disconnected (lost) network does
+     * not trigger onUnavailable() once the time-out period expires.
+     */
+    @Test
+    public void testSatisfiedThenLostNetworkRequestDoesNotTriggerOnUnavailable() throws Exception {
+        NetworkRequest nr = new NetworkRequest.Builder().addTransportType(
+                NetworkCapabilities.TRANSPORT_WIFI).build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.requestNetwork(nr, networkCallback, TEST_REQUEST_TIMEOUT_MS);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        networkCallback.expectAvailableCallbacks(mWiFiNetworkAgent, false, false, false,
+                TEST_CALLBACK_TIMEOUT_MS);
+        mWiFiNetworkAgent.disconnect();
+        networkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        // Validate that UNAVAILABLE is not called
+        networkCallback.assertNoCallback();
+    }
+
+    /**
+     * Validate that when a time-out is specified for a network request the onUnavailable()
+     * callback is called when time-out expires. Then validate that if network request is
+     * (somehow) satisfied - the callback isn't called later.
+     */
+    @Test
+    public void testTimedoutNetworkRequest() throws Exception {
+        NetworkRequest nr = new NetworkRequest.Builder().addTransportType(
+                NetworkCapabilities.TRANSPORT_WIFI).build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        final int timeoutMs = 10;
+        mCm.requestNetwork(nr, networkCallback, timeoutMs);
+
+        // pass timeout and validate that UNAVAILABLE is called
+        networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null);
+
+        // create a network satisfying request - validate that request not triggered
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        networkCallback.assertNoCallback();
+    }
+
+    /**
+     * Validate that when a network request is unregistered (cancelled), no posterior event can
+     * trigger the callback.
+     */
+    @Test
+    public void testNoCallbackAfterUnregisteredNetworkRequest() throws Exception {
+        NetworkRequest nr = new NetworkRequest.Builder().addTransportType(
+                NetworkCapabilities.TRANSPORT_WIFI).build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        final int timeoutMs = 10;
+
+        mCm.requestNetwork(nr, networkCallback, timeoutMs);
+        mCm.unregisterNetworkCallback(networkCallback);
+        // Regardless of the timeout, unregistering the callback in ConnectivityManager ensures
+        // that this callback will not be called.
+        networkCallback.assertNoCallback();
+
+        // create a network satisfying request - validate that request not triggered
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        networkCallback.assertNoCallback();
+    }
+
+    @Test
+    public void testUnfulfillableNetworkRequest() throws Exception {
+        runUnfulfillableNetworkRequest(false);
+    }
+
+    @Test
+    public void testUnfulfillableNetworkRequestAfterUnregister() throws Exception {
+        runUnfulfillableNetworkRequest(true);
+    }
+
+    /**
+     * Validate the callback flow for a factory releasing a request as unfulfillable.
+     */
+    private void runUnfulfillableNetworkRequest(boolean preUnregister) throws Exception {
+        NetworkRequest nr = new NetworkRequest.Builder().addTransportType(
+                NetworkCapabilities.TRANSPORT_WIFI).build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+
+        final HandlerThread handlerThread = new HandlerThread("testUnfulfillableNetworkRequest");
+        handlerThread.start();
+        NetworkCapabilities filter = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "testFactory", filter, mCsHandlerThread);
+        testFactory.setScoreFilter(40);
+
+        // Register the factory and expect it to receive the default request.
+        testFactory.register();
+        testFactory.expectRequestAdd();
+
+        try {
+            // Now file the test request and expect it.
+            mCm.requestNetwork(nr, networkCallback);
+            final NetworkRequest newRequest = testFactory.expectRequestAdd().request;
+
+            if (preUnregister) {
+                mCm.unregisterNetworkCallback(networkCallback);
+
+                // The request has been released : the factory should see it removed
+                // immediately.
+                testFactory.expectRequestRemove();
+
+                // Simulate the factory releasing the request as unfulfillable: no-op since
+                // the callback has already been unregistered (but a test that no exceptions are
+                // thrown).
+                testFactory.triggerUnfulfillable(newRequest);
+            } else {
+                // Simulate the factory releasing the request as unfulfillable and expect
+                // onUnavailable!
+                testFactory.triggerUnfulfillable(newRequest);
+
+                networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null);
+
+                // Declaring a request unfulfillable releases it automatically.
+                testFactory.expectRequestRemove();
+
+                // unregister network callback - a no-op (since already freed by the
+                // on-unavailable), but should not fail or throw exceptions.
+                mCm.unregisterNetworkCallback(networkCallback);
+
+                // The factory should not see any further removal, as this request has
+                // already been removed.
+            }
+        } finally {
+            testFactory.terminate();
+            handlerThread.quit();
+        }
+    }
+
+    private static class TestKeepaliveCallback extends PacketKeepaliveCallback {
+
+        public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR }
+
+        private class CallbackValue {
+            public CallbackType callbackType;
+            public int error;
+
+            public CallbackValue(CallbackType type) {
+                this.callbackType = type;
+                this.error = PacketKeepalive.SUCCESS;
+                assertTrue("onError callback must have error", type != CallbackType.ON_ERROR);
+            }
+
+            public CallbackValue(CallbackType type, int error) {
+                this.callbackType = type;
+                this.error = error;
+                assertEquals("error can only be set for onError", type, CallbackType.ON_ERROR);
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                return o instanceof CallbackValue &&
+                        this.callbackType == ((CallbackValue) o).callbackType &&
+                        this.error == ((CallbackValue) o).error;
+            }
+
+            @Override
+            public String toString() {
+                return String.format("%s(%s, %d)", getClass().getSimpleName(), callbackType, error);
+            }
+        }
+
+        private final LinkedBlockingQueue<CallbackValue> mCallbacks = new LinkedBlockingQueue<>();
+
+        @Override
+        public void onStarted() {
+            mCallbacks.add(new CallbackValue(CallbackType.ON_STARTED));
+        }
+
+        @Override
+        public void onStopped() {
+            mCallbacks.add(new CallbackValue(CallbackType.ON_STOPPED));
+        }
+
+        @Override
+        public void onError(int error) {
+            mCallbacks.add(new CallbackValue(CallbackType.ON_ERROR, error));
+        }
+
+        private void expectCallback(CallbackValue callbackValue) throws InterruptedException {
+            assertEquals(callbackValue, mCallbacks.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        }
+
+        public void expectStarted() throws Exception {
+            expectCallback(new CallbackValue(CallbackType.ON_STARTED));
+        }
+
+        public void expectStopped() throws Exception {
+            expectCallback(new CallbackValue(CallbackType.ON_STOPPED));
+        }
+
+        public void expectError(int error) throws Exception {
+            expectCallback(new CallbackValue(CallbackType.ON_ERROR, error));
+        }
+    }
+
+    private static class TestSocketKeepaliveCallback extends SocketKeepalive.Callback {
+
+        public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR };
+
+        private class CallbackValue {
+            public CallbackType callbackType;
+            public int error;
+
+            CallbackValue(CallbackType type) {
+                this.callbackType = type;
+                this.error = SocketKeepalive.SUCCESS;
+                assertTrue("onError callback must have error", type != CallbackType.ON_ERROR);
+            }
+
+            CallbackValue(CallbackType type, int error) {
+                this.callbackType = type;
+                this.error = error;
+                assertEquals("error can only be set for onError", type, CallbackType.ON_ERROR);
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                return o instanceof CallbackValue
+                        && this.callbackType == ((CallbackValue) o).callbackType
+                        && this.error == ((CallbackValue) o).error;
+            }
+
+            @Override
+            public String toString() {
+                return String.format("%s(%s, %d)", getClass().getSimpleName(), callbackType,
+                        error);
+            }
+        }
+
+        private LinkedBlockingQueue<CallbackValue> mCallbacks = new LinkedBlockingQueue<>();
+        private final Executor mExecutor;
+
+        TestSocketKeepaliveCallback(@NonNull Executor executor) {
+            mExecutor = executor;
+        }
+
+        @Override
+        public void onStarted() {
+            mCallbacks.add(new CallbackValue(CallbackType.ON_STARTED));
+        }
+
+        @Override
+        public void onStopped() {
+            mCallbacks.add(new CallbackValue(CallbackType.ON_STOPPED));
+        }
+
+        @Override
+        public void onError(int error) {
+            mCallbacks.add(new CallbackValue(CallbackType.ON_ERROR, error));
+        }
+
+        private void expectCallback(CallbackValue callbackValue) throws InterruptedException {
+            assertEquals(callbackValue, mCallbacks.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        }
+
+        public void expectStarted() throws InterruptedException {
+            expectCallback(new CallbackValue(CallbackType.ON_STARTED));
+        }
+
+        public void expectStopped() throws InterruptedException {
+            expectCallback(new CallbackValue(CallbackType.ON_STOPPED));
+        }
+
+        public void expectError(int error) throws InterruptedException {
+            expectCallback(new CallbackValue(CallbackType.ON_ERROR, error));
+        }
+
+        public void assertNoCallback() {
+            waitForIdleSerialExecutor(mExecutor, TIMEOUT_MS);
+            CallbackValue cv = mCallbacks.peek();
+            assertNull("Unexpected callback: " + cv, cv);
+        }
+    }
+
+    private Network connectKeepaliveNetwork(LinkProperties lp) throws Exception {
+        // Ensure the network is disconnected before anything else occurs
+        if (mWiFiNetworkAgent != null) {
+            assertNull(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()));
+        }
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
+        mWiFiNetworkAgent.connect(true);
+        b.expectBroadcast();
+        verifyActiveNetwork(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.sendLinkProperties(lp);
+        waitForIdle();
+        return mWiFiNetworkAgent.getNetwork();
+    }
+
+    @Test
+    public void testPacketKeepalives() throws Exception {
+        InetAddress myIPv4 = InetAddress.getByName("192.0.2.129");
+        InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35");
+        InetAddress myIPv6 = InetAddress.getByName("2001:db8::1");
+        InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8");
+        InetAddress dstIPv6 = InetAddress.getByName("2001:4860:4860::8888");
+
+        final int validKaInterval = 15;
+        final int invalidKaInterval = 9;
+
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("wlan12");
+        lp.addLinkAddress(new LinkAddress(myIPv6, 64));
+        lp.addLinkAddress(new LinkAddress(myIPv4, 25));
+        lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234")));
+        lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254")));
+
+        Network notMyNet = new Network(61234);
+        Network myNet = connectKeepaliveNetwork(lp);
+
+        TestKeepaliveCallback callback = new TestKeepaliveCallback();
+        PacketKeepalive ka;
+
+        // Attempt to start keepalives with invalid parameters and check for errors.
+        ka = mCm.startNattKeepalive(notMyNet, validKaInterval, callback, myIPv4, 1234, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_NETWORK);
+
+        ka = mCm.startNattKeepalive(myNet, invalidKaInterval, callback, myIPv4, 1234, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_INTERVAL);
+
+        ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 1234, dstIPv6);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
+
+        ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv6, 1234, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
+
+        // NAT-T is only supported for IPv4.
+        ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv6, 1234, dstIPv6);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
+
+        ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 123456, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_PORT);
+
+        ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 123456, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_PORT);
+
+        ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED);
+
+        ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4);
+        callback.expectError(PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED);
+
+        // Check that a started keepalive can be stopped.
+        mWiFiNetworkAgent.setStartKeepaliveEvent(PacketKeepalive.SUCCESS);
+        ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4);
+        callback.expectStarted();
+        mWiFiNetworkAgent.setStopKeepaliveEvent(PacketKeepalive.SUCCESS);
+        ka.stop();
+        callback.expectStopped();
+
+        // Check that deleting the IP address stops the keepalive.
+        LinkProperties bogusLp = new LinkProperties(lp);
+        ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4);
+        callback.expectStarted();
+        bogusLp.removeLinkAddress(new LinkAddress(myIPv4, 25));
+        bogusLp.addLinkAddress(new LinkAddress(notMyIPv4, 25));
+        mWiFiNetworkAgent.sendLinkProperties(bogusLp);
+        callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
+        mWiFiNetworkAgent.sendLinkProperties(lp);
+
+        // Check that a started keepalive is stopped correctly when the network disconnects.
+        ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4);
+        callback.expectStarted();
+        mWiFiNetworkAgent.disconnect();
+        mWiFiNetworkAgent.expectDisconnected();
+        callback.expectError(PacketKeepalive.ERROR_INVALID_NETWORK);
+
+        // ... and that stopping it after that has no adverse effects.
+        waitForIdle();
+        final Network myNetAlias = myNet;
+        assertNull(mCm.getNetworkCapabilities(myNetAlias));
+        ka.stop();
+
+        // Reconnect.
+        myNet = connectKeepaliveNetwork(lp);
+        mWiFiNetworkAgent.setStartKeepaliveEvent(PacketKeepalive.SUCCESS);
+
+        // Check that keepalive slots start from 1 and increment. The first one gets slot 1.
+        mWiFiNetworkAgent.setExpectedKeepaliveSlot(1);
+        ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4);
+        callback.expectStarted();
+
+        // The second one gets slot 2.
+        mWiFiNetworkAgent.setExpectedKeepaliveSlot(2);
+        TestKeepaliveCallback callback2 = new TestKeepaliveCallback();
+        PacketKeepalive ka2 = mCm.startNattKeepalive(
+                myNet, validKaInterval, callback2, myIPv4, 6789, dstIPv4);
+        callback2.expectStarted();
+
+        // Now stop the first one and create a third. This also gets slot 1.
+        ka.stop();
+        callback.expectStopped();
+
+        mWiFiNetworkAgent.setExpectedKeepaliveSlot(1);
+        TestKeepaliveCallback callback3 = new TestKeepaliveCallback();
+        PacketKeepalive ka3 = mCm.startNattKeepalive(
+                myNet, validKaInterval, callback3, myIPv4, 9876, dstIPv4);
+        callback3.expectStarted();
+
+        ka2.stop();
+        callback2.expectStopped();
+
+        ka3.stop();
+        callback3.expectStopped();
+    }
+
+    // Helper method to prepare the executor and run test
+    private void runTestWithSerialExecutors(ExceptionUtils.ThrowingConsumer<Executor> functor)
+            throws Exception {
+        final ExecutorService executorSingleThread = Executors.newSingleThreadExecutor();
+        final Executor executorInline = (Runnable r) -> r.run();
+        functor.accept(executorSingleThread);
+        executorSingleThread.shutdown();
+        functor.accept(executorInline);
+    }
+
+    @Test
+    public void testNattSocketKeepalives() throws Exception {
+        runTestWithSerialExecutors(executor -> doTestNattSocketKeepalivesWithExecutor(executor));
+        runTestWithSerialExecutors(executor -> doTestNattSocketKeepalivesFdWithExecutor(executor));
+    }
+
+    private void doTestNattSocketKeepalivesWithExecutor(Executor executor) throws Exception {
+        // TODO: 1. Move this outside of ConnectivityServiceTest.
+        //       2. Make test to verify that Nat-T keepalive socket is created by IpSecService.
+        //       3. Mock ipsec service.
+        final InetAddress myIPv4 = InetAddress.getByName("192.0.2.129");
+        final InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35");
+        final InetAddress myIPv6 = InetAddress.getByName("2001:db8::1");
+        final InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8");
+        final InetAddress dstIPv6 = InetAddress.getByName("2001:4860:4860::8888");
+
+        final int validKaInterval = 15;
+        final int invalidKaInterval = 9;
+
+        final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE);
+        final UdpEncapsulationSocket testSocket = mIpSec.openUdpEncapsulationSocket();
+        final int srcPort = testSocket.getPort();
+
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("wlan12");
+        lp.addLinkAddress(new LinkAddress(myIPv6, 64));
+        lp.addLinkAddress(new LinkAddress(myIPv4, 25));
+        lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234")));
+        lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254")));
+
+        Network notMyNet = new Network(61234);
+        Network myNet = connectKeepaliveNetwork(lp);
+
+        TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(executor);
+
+        // Attempt to start keepalives with invalid parameters and check for errors.
+        // Invalid network.
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+                notMyNet, testSocket, myIPv4, dstIPv4, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK);
+        }
+
+        // Invalid interval.
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+                myNet, testSocket, myIPv4, dstIPv4, executor, callback)) {
+            ka.start(invalidKaInterval);
+            callback.expectError(SocketKeepalive.ERROR_INVALID_INTERVAL);
+        }
+
+        // Invalid destination.
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+                myNet, testSocket, myIPv4, dstIPv6, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS);
+        }
+
+        // Invalid source;
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+                myNet, testSocket, myIPv6, dstIPv4, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS);
+        }
+
+        // NAT-T is only supported for IPv4.
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+                myNet, testSocket, myIPv6, dstIPv6, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS);
+        }
+
+        // Basic check before testing started keepalive.
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+                myNet, testSocket, myIPv4, dstIPv4, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectError(SocketKeepalive.ERROR_UNSUPPORTED);
+        }
+
+        // Check that a started keepalive can be stopped.
+        mWiFiNetworkAgent.setStartKeepaliveEvent(SocketKeepalive.SUCCESS);
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+                myNet, testSocket, myIPv4, dstIPv4, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectStarted();
+            mWiFiNetworkAgent.setStopKeepaliveEvent(SocketKeepalive.SUCCESS);
+            ka.stop();
+            callback.expectStopped();
+
+            // Check that keepalive could be restarted.
+            ka.start(validKaInterval);
+            callback.expectStarted();
+            ka.stop();
+            callback.expectStopped();
+
+            // Check that keepalive can be restarted without waiting for callback.
+            ka.start(validKaInterval);
+            callback.expectStarted();
+            ka.stop();
+            ka.start(validKaInterval);
+            callback.expectStopped();
+            callback.expectStarted();
+            ka.stop();
+            callback.expectStopped();
+        }
+
+        // Check that deleting the IP address stops the keepalive.
+        LinkProperties bogusLp = new LinkProperties(lp);
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+                myNet, testSocket, myIPv4, dstIPv4, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectStarted();
+            bogusLp.removeLinkAddress(new LinkAddress(myIPv4, 25));
+            bogusLp.addLinkAddress(new LinkAddress(notMyIPv4, 25));
+            mWiFiNetworkAgent.sendLinkProperties(bogusLp);
+            callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS);
+            mWiFiNetworkAgent.sendLinkProperties(lp);
+        }
+
+        // Check that a started keepalive is stopped correctly when the network disconnects.
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+                myNet, testSocket, myIPv4, dstIPv4, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectStarted();
+            mWiFiNetworkAgent.disconnect();
+            mWiFiNetworkAgent.expectDisconnected();
+            callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK);
+
+            // ... and that stopping it after that has no adverse effects.
+            waitForIdle();
+            final Network myNetAlias = myNet;
+            assertNull(mCm.getNetworkCapabilities(myNetAlias));
+            ka.stop();
+            callback.assertNoCallback();
+        }
+
+        // Reconnect.
+        myNet = connectKeepaliveNetwork(lp);
+        mWiFiNetworkAgent.setStartKeepaliveEvent(SocketKeepalive.SUCCESS);
+
+        // Check that a stop followed by network disconnects does not result in crash.
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+                myNet, testSocket, myIPv4, dstIPv4, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectStarted();
+            // Delay the response of keepalive events in networkAgent long enough to make sure
+            // the follow-up network disconnection will be processed first.
+            mWiFiNetworkAgent.setKeepaliveResponseDelay(3 * TIMEOUT_MS);
+            ka.stop();
+
+            // Make sure the stop has been processed. Wait for executor idle is needed to prevent
+            // flaky since the actual stop call to the service is delegated to executor thread.
+            waitForIdleSerialExecutor(executor, TIMEOUT_MS);
+            waitForIdle();
+
+            mWiFiNetworkAgent.disconnect();
+            mWiFiNetworkAgent.expectDisconnected();
+            callback.expectStopped();
+            callback.assertNoCallback();
+        }
+
+        // Reconnect.
+        waitForIdle();
+        myNet = connectKeepaliveNetwork(lp);
+        mWiFiNetworkAgent.setStartKeepaliveEvent(SocketKeepalive.SUCCESS);
+
+        // Check that keepalive slots start from 1 and increment. The first one gets slot 1.
+        mWiFiNetworkAgent.setExpectedKeepaliveSlot(1);
+        int srcPort2 = 0;
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+                myNet, testSocket, myIPv4, dstIPv4, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectStarted();
+
+            // The second one gets slot 2.
+            mWiFiNetworkAgent.setExpectedKeepaliveSlot(2);
+            final UdpEncapsulationSocket testSocket2 = mIpSec.openUdpEncapsulationSocket();
+            srcPort2 = testSocket2.getPort();
+            TestSocketKeepaliveCallback callback2 = new TestSocketKeepaliveCallback(executor);
+            try (SocketKeepalive ka2 = mCm.createSocketKeepalive(
+                    myNet, testSocket2, myIPv4, dstIPv4, executor, callback2)) {
+                ka2.start(validKaInterval);
+                callback2.expectStarted();
+
+                ka.stop();
+                callback.expectStopped();
+
+                ka2.stop();
+                callback2.expectStopped();
+
+                testSocket.close();
+                testSocket2.close();
+            }
+        }
+
+        // Check that there is no port leaked after all keepalives and sockets are closed.
+        // TODO: enable this check after ensuring a valid free port. See b/129512753#comment7.
+        // assertFalse(isUdpPortInUse(srcPort));
+        // assertFalse(isUdpPortInUse(srcPort2));
+
+        mWiFiNetworkAgent.disconnect();
+        mWiFiNetworkAgent.expectDisconnected();
+        mWiFiNetworkAgent = null;
+    }
+
+    @Test
+    public void testTcpSocketKeepalives() throws Exception {
+        runTestWithSerialExecutors(executor -> doTestTcpSocketKeepalivesWithExecutor(executor));
+    }
+
+    private void doTestTcpSocketKeepalivesWithExecutor(Executor executor) throws Exception {
+        final int srcPortV4 = 12345;
+        final int srcPortV6 = 23456;
+        final InetAddress myIPv4 = InetAddress.getByName("127.0.0.1");
+        final InetAddress myIPv6 = InetAddress.getByName("::1");
+
+        final int validKaInterval = 15;
+
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("wlan12");
+        lp.addLinkAddress(new LinkAddress(myIPv6, 64));
+        lp.addLinkAddress(new LinkAddress(myIPv4, 25));
+        lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234")));
+        lp.addRoute(new RouteInfo(InetAddress.getByName("127.0.0.254")));
+
+        final Network notMyNet = new Network(61234);
+        final Network myNet = connectKeepaliveNetwork(lp);
+
+        final Socket testSocketV4 = new Socket();
+        final Socket testSocketV6 = new Socket();
+
+        TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(executor);
+
+        // Attempt to start Tcp keepalives with invalid parameters and check for errors.
+        // Invalid network.
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+            notMyNet, testSocketV4, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK);
+        }
+
+        // Invalid Socket (socket is not bound with IPv4 address).
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+            myNet, testSocketV4, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectError(SocketKeepalive.ERROR_INVALID_SOCKET);
+        }
+
+        // Invalid Socket (socket is not bound with IPv6 address).
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+            myNet, testSocketV6, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectError(SocketKeepalive.ERROR_INVALID_SOCKET);
+        }
+
+        // Bind the socket address
+        testSocketV4.bind(new InetSocketAddress(myIPv4, srcPortV4));
+        testSocketV6.bind(new InetSocketAddress(myIPv6, srcPortV6));
+
+        // Invalid Socket (socket is bound with IPv4 address).
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+            myNet, testSocketV4, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectError(SocketKeepalive.ERROR_INVALID_SOCKET);
+        }
+
+        // Invalid Socket (socket is bound with IPv6 address).
+        try (SocketKeepalive ka = mCm.createSocketKeepalive(
+            myNet, testSocketV6, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectError(SocketKeepalive.ERROR_INVALID_SOCKET);
+        }
+
+        testSocketV4.close();
+        testSocketV6.close();
+
+        mWiFiNetworkAgent.disconnect();
+        mWiFiNetworkAgent.expectDisconnected();
+        mWiFiNetworkAgent = null;
+    }
+
+    private void doTestNattSocketKeepalivesFdWithExecutor(Executor executor) throws Exception {
+        final InetAddress myIPv4 = InetAddress.getByName("192.0.2.129");
+        final InetAddress anyIPv4 = InetAddress.getByName("0.0.0.0");
+        final InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8");
+        final int validKaInterval = 15;
+
+        // Prepare the target network.
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("wlan12");
+        lp.addLinkAddress(new LinkAddress(myIPv4, 25));
+        lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254")));
+        Network myNet = connectKeepaliveNetwork(lp);
+        mWiFiNetworkAgent.setStartKeepaliveEvent(SocketKeepalive.SUCCESS);
+        mWiFiNetworkAgent.setStopKeepaliveEvent(SocketKeepalive.SUCCESS);
+
+        TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(executor);
+
+        // Prepare the target file descriptor, keep only one instance.
+        final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE);
+        final UdpEncapsulationSocket testSocket = mIpSec.openUdpEncapsulationSocket();
+        final int srcPort = testSocket.getPort();
+        final ParcelFileDescriptor testPfd =
+                ParcelFileDescriptor.dup(testSocket.getFileDescriptor());
+        testSocket.close();
+        assertTrue(isUdpPortInUse(srcPort));
+
+        // Start keepalive and explicit make the variable goes out of scope with try-with-resources
+        // block.
+        try (SocketKeepalive ka = mCm.createNattKeepalive(
+                myNet, testPfd, myIPv4, dstIPv4, executor, callback)) {
+            ka.start(validKaInterval);
+            callback.expectStarted();
+            ka.stop();
+            callback.expectStopped();
+        }
+
+        // Check that the ParcelFileDescriptor is still valid after keepalive stopped,
+        // ErrnoException with EBADF will be thrown if the socket is closed when checking local
+        // address.
+        assertTrue(isUdpPortInUse(srcPort));
+        final InetSocketAddress sa =
+                (InetSocketAddress) Os.getsockname(testPfd.getFileDescriptor());
+        assertEquals(anyIPv4, sa.getAddress());
+
+        testPfd.close();
+        // TODO: enable this check after ensuring a valid free port. See b/129512753#comment7.
+        // assertFalse(isUdpPortInUse(srcPort));
+
+        mWiFiNetworkAgent.disconnect();
+        mWiFiNetworkAgent.expectDisconnected();
+        mWiFiNetworkAgent = null;
+    }
+
+    private static boolean isUdpPortInUse(int port) {
+        try (DatagramSocket ignored = new DatagramSocket(port)) {
+            return false;
+        } catch (IOException alreadyInUse) {
+            return true;
+        }
+    }
+
+    @Test
+    public void testGetCaptivePortalServerUrl() throws Exception {
+        String url = mCm.getCaptivePortalServerUrl();
+        assertEquals("http://connectivitycheck.gstatic.com/generate_204", url);
+    }
+
+    private static class TestNetworkPinner extends NetworkPinner {
+        public static boolean awaitPin(int timeoutMs) throws InterruptedException {
+            synchronized(sLock) {
+                if (sNetwork == null) {
+                    sLock.wait(timeoutMs);
+                }
+                return sNetwork != null;
+            }
+        }
+
+        public static boolean awaitUnpin(int timeoutMs) throws InterruptedException {
+            synchronized(sLock) {
+                if (sNetwork != null) {
+                    sLock.wait(timeoutMs);
+                }
+                return sNetwork == null;
+            }
+        }
+    }
+
+    private void assertPinnedToWifiWithCellDefault() {
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getBoundNetworkForProcess());
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+    }
+
+    private void assertPinnedToWifiWithWifiDefault() {
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getBoundNetworkForProcess());
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+    }
+
+    private void assertNotPinnedToWifi() {
+        assertNull(mCm.getBoundNetworkForProcess());
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+    }
+
+    @Test
+    public void testNetworkPinner() throws Exception {
+        NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .build();
+        assertNull(mCm.getBoundNetworkForProcess());
+
+        TestNetworkPinner.pin(mServiceContext, wifiRequest);
+        assertNull(mCm.getBoundNetworkForProcess());
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+
+        // When wi-fi connects, expect to be pinned.
+        assertTrue(TestNetworkPinner.awaitPin(100));
+        assertPinnedToWifiWithCellDefault();
+
+        // Disconnect and expect the pin to drop.
+        mWiFiNetworkAgent.disconnect();
+        assertTrue(TestNetworkPinner.awaitUnpin(100));
+        assertNotPinnedToWifi();
+
+        // Reconnecting does not cause the pin to come back.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        assertFalse(TestNetworkPinner.awaitPin(100));
+        assertNotPinnedToWifi();
+
+        // Pinning while connected causes the pin to take effect immediately.
+        TestNetworkPinner.pin(mServiceContext, wifiRequest);
+        assertTrue(TestNetworkPinner.awaitPin(100));
+        assertPinnedToWifiWithCellDefault();
+
+        // Explicitly unpin and expect to use the default network again.
+        TestNetworkPinner.unpin();
+        assertNotPinnedToWifi();
+
+        // Disconnect cell and wifi.
+        ExpectedBroadcast b = registerConnectivityBroadcast(3);  // cell down, wifi up, wifi down.
+        mCellNetworkAgent.disconnect();
+        mWiFiNetworkAgent.disconnect();
+        b.expectBroadcast();
+
+        // Pinning takes effect even if the pinned network is the default when the pin is set...
+        TestNetworkPinner.pin(mServiceContext, wifiRequest);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        assertTrue(TestNetworkPinner.awaitPin(100));
+        assertPinnedToWifiWithWifiDefault();
+
+        // ... and is maintained even when that network is no longer the default.
+        b = registerConnectivityBroadcast(1);
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mCellNetworkAgent.connect(true);
+        b.expectBroadcast();
+        assertPinnedToWifiWithCellDefault();
+    }
+
+    @Test
+    public void testNetworkCallbackMaximum() throws Exception {
+        final int MAX_REQUESTS = 100;
+        final int CALLBACKS = 89;
+        final int INTENTS = 11;
+        final int SYSTEM_ONLY_MAX_REQUESTS = 250;
+        assertEquals(MAX_REQUESTS, CALLBACKS + INTENTS);
+
+        NetworkRequest networkRequest = new NetworkRequest.Builder().build();
+        ArrayList<Object> registered = new ArrayList<>();
+
+        int j = 0;
+        while (j++ < CALLBACKS / 2) {
+            NetworkCallback cb = new NetworkCallback();
+            mCm.requestNetwork(networkRequest, cb);
+            registered.add(cb);
+        }
+        while (j++ < CALLBACKS) {
+            NetworkCallback cb = new NetworkCallback();
+            mCm.registerNetworkCallback(networkRequest, cb);
+            registered.add(cb);
+        }
+        j = 0;
+        while (j++ < INTENTS / 2) {
+            final PendingIntent pi = PendingIntent.getBroadcast(mContext, 0 /* requestCode */,
+                    new Intent("a" + j), FLAG_IMMUTABLE);
+            mCm.requestNetwork(networkRequest, pi);
+            registered.add(pi);
+        }
+        while (j++ < INTENTS) {
+            final PendingIntent pi = PendingIntent.getBroadcast(mContext, 0 /* requestCode */,
+                    new Intent("b" + j), FLAG_IMMUTABLE);
+            mCm.registerNetworkCallback(networkRequest, pi);
+            registered.add(pi);
+        }
+
+        // Test that the limit is enforced when MAX_REQUESTS simultaneous requests are added.
+        assertThrows(TooManyRequestsException.class, () ->
+                mCm.requestNetwork(networkRequest, new NetworkCallback())
+        );
+        assertThrows(TooManyRequestsException.class, () ->
+                mCm.registerNetworkCallback(networkRequest, new NetworkCallback())
+        );
+        assertThrows(TooManyRequestsException.class, () ->
+                mCm.requestNetwork(networkRequest,
+                        PendingIntent.getBroadcast(mContext, 0 /* requestCode */,
+                                new Intent("c"), FLAG_IMMUTABLE))
+        );
+        assertThrows(TooManyRequestsException.class, () ->
+                mCm.registerNetworkCallback(networkRequest,
+                        PendingIntent.getBroadcast(mContext, 0 /* requestCode */,
+                                new Intent("d"), FLAG_IMMUTABLE))
+        );
+
+        // The system gets another SYSTEM_ONLY_MAX_REQUESTS slots.
+        final Handler handler = new Handler(ConnectivityThread.getInstanceLooper());
+        withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, () -> {
+            ArrayList<NetworkCallback> systemRegistered = new ArrayList<>();
+            for (int i = 0; i < SYSTEM_ONLY_MAX_REQUESTS - 1; i++) {
+                NetworkCallback cb = new NetworkCallback();
+                if (i % 2 == 0) {
+                    mCm.registerDefaultNetworkCallbackForUid(1000000 + i, cb, handler);
+                } else {
+                    mCm.registerNetworkCallback(networkRequest, cb);
+                }
+                systemRegistered.add(cb);
+            }
+            waitForIdle();
+
+            assertThrows(TooManyRequestsException.class, () ->
+                    mCm.registerDefaultNetworkCallbackForUid(1001042, new NetworkCallback(),
+                            handler));
+            assertThrows(TooManyRequestsException.class, () ->
+                    mCm.registerNetworkCallback(networkRequest, new NetworkCallback()));
+
+            for (NetworkCallback callback : systemRegistered) {
+                mCm.unregisterNetworkCallback(callback);
+            }
+            waitForIdle();  // Wait for requests to be unregistered before giving up the permission.
+        });
+
+        for (Object o : registered) {
+            if (o instanceof NetworkCallback) {
+                mCm.unregisterNetworkCallback((NetworkCallback)o);
+            }
+            if (o instanceof PendingIntent) {
+                mCm.unregisterNetworkCallback((PendingIntent)o);
+            }
+        }
+        waitForIdle();
+
+        // Test that the limit is not hit when MAX_REQUESTS requests are added and removed.
+        for (int i = 0; i < MAX_REQUESTS; i++) {
+            NetworkCallback networkCallback = new NetworkCallback();
+            mCm.requestNetwork(networkRequest, networkCallback);
+            mCm.unregisterNetworkCallback(networkCallback);
+        }
+        waitForIdle();
+
+        for (int i = 0; i < MAX_REQUESTS; i++) {
+            NetworkCallback networkCallback = new NetworkCallback();
+            mCm.registerNetworkCallback(networkRequest, networkCallback);
+            mCm.unregisterNetworkCallback(networkCallback);
+        }
+        waitForIdle();
+
+        for (int i = 0; i < MAX_REQUESTS; i++) {
+            NetworkCallback networkCallback = new NetworkCallback();
+            mCm.registerDefaultNetworkCallback(networkCallback);
+            mCm.unregisterNetworkCallback(networkCallback);
+        }
+        waitForIdle();
+
+        for (int i = 0; i < MAX_REQUESTS; i++) {
+            NetworkCallback networkCallback = new NetworkCallback();
+            mCm.registerDefaultNetworkCallback(networkCallback);
+            mCm.unregisterNetworkCallback(networkCallback);
+        }
+        waitForIdle();
+
+        withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, () -> {
+            for (int i = 0; i < MAX_REQUESTS; i++) {
+                NetworkCallback networkCallback = new NetworkCallback();
+                mCm.registerDefaultNetworkCallbackForUid(1000000 + i, networkCallback,
+                        new Handler(ConnectivityThread.getInstanceLooper()));
+                mCm.unregisterNetworkCallback(networkCallback);
+            }
+        });
+        waitForIdle();
+
+        for (int i = 0; i < MAX_REQUESTS; i++) {
+            final PendingIntent pendingIntent = PendingIntent.getBroadcast(
+                    mContext, 0 /* requestCode */, new Intent("e" + i), FLAG_IMMUTABLE);
+            mCm.requestNetwork(networkRequest, pendingIntent);
+            mCm.unregisterNetworkCallback(pendingIntent);
+        }
+        waitForIdle();
+
+        for (int i = 0; i < MAX_REQUESTS; i++) {
+            final PendingIntent pendingIntent = PendingIntent.getBroadcast(
+                    mContext, 0 /* requestCode */, new Intent("f" + i), FLAG_IMMUTABLE);
+            mCm.registerNetworkCallback(networkRequest, pendingIntent);
+            mCm.unregisterNetworkCallback(pendingIntent);
+        }
+    }
+
+    @Test
+    public void testNetworkInfoOfTypeNone() throws Exception {
+        ExpectedBroadcast b = registerConnectivityBroadcast(1);
+
+        verifyNoNetwork();
+        TestNetworkAgentWrapper wifiAware = new TestNetworkAgentWrapper(TRANSPORT_WIFI_AWARE);
+        assertNull(mCm.getActiveNetworkInfo());
+
+        Network[] allNetworks = mCm.getAllNetworks();
+        assertLength(1, allNetworks);
+        Network network = allNetworks[0];
+        NetworkCapabilities capabilities = mCm.getNetworkCapabilities(network);
+        assertTrue(capabilities.hasTransport(TRANSPORT_WIFI_AWARE));
+
+        final NetworkRequest request =
+                new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI_AWARE).build();
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+
+        // Bring up wifi aware network.
+        wifiAware.connect(false, false, false /* isStrictMode */);
+        callback.expectAvailableCallbacksUnvalidated(wifiAware);
+
+        assertNull(mCm.getActiveNetworkInfo());
+        assertNull(mCm.getActiveNetwork());
+        // TODO: getAllNetworkInfo is dirty and returns a non-empty array right from the start
+        // of this test. Fix it and uncomment the assert below.
+        //assertEmpty(mCm.getAllNetworkInfo());
+
+        // Disconnect wifi aware network.
+        wifiAware.disconnect();
+        callback.expectCallbackThat(TIMEOUT_MS, (info) -> info instanceof CallbackEntry.Lost);
+        mCm.unregisterNetworkCallback(callback);
+
+        verifyNoNetwork();
+        b.expectNoBroadcast(10);
+    }
+
+    @Test
+    public void testDeprecatedAndUnsupportedOperations() throws Exception {
+        final int TYPE_NONE = ConnectivityManager.TYPE_NONE;
+        assertNull(mCm.getNetworkInfo(TYPE_NONE));
+        assertNull(mCm.getNetworkForType(TYPE_NONE));
+        assertNull(mCm.getLinkProperties(TYPE_NONE));
+        assertFalse(mCm.isNetworkSupported(TYPE_NONE));
+
+        assertThrows(IllegalArgumentException.class,
+                () -> mCm.networkCapabilitiesForType(TYPE_NONE));
+
+        Class<UnsupportedOperationException> unsupported = UnsupportedOperationException.class;
+        assertThrows(unsupported, () -> mCm.startUsingNetworkFeature(TYPE_WIFI, ""));
+        assertThrows(unsupported, () -> mCm.stopUsingNetworkFeature(TYPE_WIFI, ""));
+        // TODO: let test context have configuration application target sdk version
+        // and test that pre-M requesting for TYPE_NONE sends back APN_REQUEST_FAILED
+        assertThrows(unsupported, () -> mCm.startUsingNetworkFeature(TYPE_NONE, ""));
+        assertThrows(unsupported, () -> mCm.stopUsingNetworkFeature(TYPE_NONE, ""));
+        assertThrows(unsupported, () -> mCm.requestRouteToHostAddress(TYPE_NONE, null));
+    }
+
+    @Test
+    public void testLinkPropertiesEnsuresDirectlyConnectedRoutes() throws Exception {
+        final NetworkRequest networkRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(networkRequest, networkCallback);
+
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(WIFI_IFNAME);
+        LinkAddress myIpv4Address = new LinkAddress("192.168.12.3/24");
+        RouteInfo myIpv4DefaultRoute = new RouteInfo((IpPrefix) null,
+                InetAddresses.parseNumericAddress("192.168.12.1"), lp.getInterfaceName());
+        lp.addLinkAddress(myIpv4Address);
+        lp.addRoute(myIpv4DefaultRoute);
+
+        // Verify direct routes are added when network agent is first registered in
+        // ConnectivityService.
+        TestNetworkAgentWrapper networkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp);
+        networkAgent.connect(true);
+        networkCallback.expectCallback(CallbackEntry.AVAILABLE, networkAgent);
+        networkCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, networkAgent);
+        CallbackEntry.LinkPropertiesChanged cbi =
+                networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED,
+                networkAgent);
+        networkCallback.expectCallback(CallbackEntry.BLOCKED_STATUS, networkAgent);
+        networkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, networkAgent);
+        networkCallback.assertNoCallback();
+        checkDirectlyConnectedRoutes(cbi.getLp(), Arrays.asList(myIpv4Address),
+                Arrays.asList(myIpv4DefaultRoute));
+        checkDirectlyConnectedRoutes(mCm.getLinkProperties(networkAgent.getNetwork()),
+                Arrays.asList(myIpv4Address), Arrays.asList(myIpv4DefaultRoute));
+
+        // Verify direct routes are added during subsequent link properties updates.
+        LinkProperties newLp = new LinkProperties(lp);
+        LinkAddress myIpv6Address1 = new LinkAddress("fe80::cafe/64");
+        LinkAddress myIpv6Address2 = new LinkAddress("2001:db8::2/64");
+        newLp.addLinkAddress(myIpv6Address1);
+        newLp.addLinkAddress(myIpv6Address2);
+        networkAgent.sendLinkProperties(newLp);
+        cbi = networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, networkAgent);
+        networkCallback.assertNoCallback();
+        checkDirectlyConnectedRoutes(cbi.getLp(),
+                Arrays.asList(myIpv4Address, myIpv6Address1, myIpv6Address2),
+                Arrays.asList(myIpv4DefaultRoute));
+        mCm.unregisterNetworkCallback(networkCallback);
+    }
+
+    private void expectNotifyNetworkStatus(List<Network> networks, String defaultIface,
+            Integer vpnUid, String vpnIfname, List<String> underlyingIfaces) throws Exception {
+        ArgumentCaptor<List<Network>> networksCaptor = ArgumentCaptor.forClass(List.class);
+        ArgumentCaptor<List<UnderlyingNetworkInfo>> vpnInfosCaptor =
+                ArgumentCaptor.forClass(List.class);
+
+        verify(mStatsManager, atLeastOnce()).notifyNetworkStatus(networksCaptor.capture(),
+                any(List.class), eq(defaultIface), vpnInfosCaptor.capture());
+
+        assertSameElements(networksCaptor.getValue(), networks);
+
+        List<UnderlyingNetworkInfo> infos = vpnInfosCaptor.getValue();
+        if (vpnUid != null) {
+            assertEquals("Should have exactly one VPN:", 1, infos.size());
+            UnderlyingNetworkInfo info = infos.get(0);
+            assertEquals("Unexpected VPN owner:", (int) vpnUid, info.getOwnerUid());
+            assertEquals("Unexpected VPN interface:", vpnIfname, info.getInterface());
+            assertSameElements(underlyingIfaces, info.getUnderlyingInterfaces());
+        } else {
+            assertEquals(0, infos.size());
+            return;
+        }
+    }
+
+    private void expectNotifyNetworkStatus(
+            List<Network> networks, String defaultIface) throws Exception {
+        expectNotifyNetworkStatus(networks, defaultIface, null, null, List.of());
+    }
+
+    @Test
+    public void testStatsIfacesChanged() throws Exception {
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+
+        final List<Network> onlyCell = List.of(mCellNetworkAgent.getNetwork());
+        final List<Network> onlyWifi = List.of(mWiFiNetworkAgent.getNetwork());
+
+        LinkProperties cellLp = new LinkProperties();
+        cellLp.setInterfaceName(MOBILE_IFNAME);
+        LinkProperties wifiLp = new LinkProperties();
+        wifiLp.setInterfaceName(WIFI_IFNAME);
+
+        // Simple connection should have updated ifaces
+        mCellNetworkAgent.connect(false);
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        waitForIdle();
+        expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME);
+        reset(mStatsManager);
+
+        // Default network switch should update ifaces.
+        mWiFiNetworkAgent.connect(false);
+        mWiFiNetworkAgent.sendLinkProperties(wifiLp);
+        waitForIdle();
+        assertEquals(wifiLp, mService.getActiveLinkProperties());
+        expectNotifyNetworkStatus(onlyWifi, WIFI_IFNAME);
+        reset(mStatsManager);
+
+        // Disconnect should update ifaces.
+        mWiFiNetworkAgent.disconnect();
+        waitForIdle();
+        expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME);
+        reset(mStatsManager);
+
+        // Metered change should update ifaces
+        mCellNetworkAgent.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+        waitForIdle();
+        expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME);
+        reset(mStatsManager);
+
+        mCellNetworkAgent.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+        waitForIdle();
+        expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME);
+        reset(mStatsManager);
+
+        // Temp metered change shouldn't update ifaces
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
+        waitForIdle();
+        verify(mStatsManager, never()).notifyNetworkStatus(eq(onlyCell),
+                any(List.class), eq(MOBILE_IFNAME), any(List.class));
+        reset(mStatsManager);
+
+        // Roaming change should update ifaces
+        mCellNetworkAgent.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
+        waitForIdle();
+        expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME);
+        reset(mStatsManager);
+
+        // Test VPNs.
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(VPN_IFNAME);
+
+        mMockVpn.establishForMyUid(lp);
+        assertUidRangesUpdatedForMyUid(true);
+
+        final List<Network> cellAndVpn =
+                List.of(mCellNetworkAgent.getNetwork(), mMockVpn.getNetwork());
+
+        // A VPN with default (null) underlying networks sets the underlying network's interfaces...
+        expectNotifyNetworkStatus(cellAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
+                List.of(MOBILE_IFNAME));
+
+        // ...and updates them as the default network switches.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        mWiFiNetworkAgent.sendLinkProperties(wifiLp);
+        final Network[] onlyNull = new Network[]{null};
+        final List<Network> wifiAndVpn =
+                List.of(mWiFiNetworkAgent.getNetwork(), mMockVpn.getNetwork());
+        final List<Network> cellAndWifi =
+                List.of(mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork());
+        final Network[] cellNullAndWifi =
+                new Network[]{mCellNetworkAgent.getNetwork(), null, mWiFiNetworkAgent.getNetwork()};
+
+        waitForIdle();
+        assertEquals(wifiLp, mService.getActiveLinkProperties());
+        expectNotifyNetworkStatus(wifiAndVpn, WIFI_IFNAME, Process.myUid(), VPN_IFNAME,
+                List.of(WIFI_IFNAME));
+        reset(mStatsManager);
+
+        // A VPN that sets its underlying networks passes the underlying interfaces, and influences
+        // the default interface sent to NetworkStatsService by virtue of applying to the system
+        // server UID (or, in this test, to the test's UID). This is the reason for sending
+        // MOBILE_IFNAME even though the default network is wifi.
+        // TODO: fix this to pass in the actual default network interface. Whether or not the VPN
+        // applies to the system server UID should not have any bearing on network stats.
+        mMockVpn.setUnderlyingNetworks(onlyCell.toArray(new Network[0]));
+        waitForIdle();
+        expectNotifyNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
+                List.of(MOBILE_IFNAME));
+        reset(mStatsManager);
+
+        mMockVpn.setUnderlyingNetworks(cellAndWifi.toArray(new Network[0]));
+        waitForIdle();
+        expectNotifyNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
+                List.of(MOBILE_IFNAME, WIFI_IFNAME));
+        reset(mStatsManager);
+
+        // Null underlying networks are ignored.
+        mMockVpn.setUnderlyingNetworks(cellNullAndWifi);
+        waitForIdle();
+        expectNotifyNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
+                List.of(MOBILE_IFNAME, WIFI_IFNAME));
+        reset(mStatsManager);
+
+        // If an underlying network disconnects, that interface should no longer be underlying.
+        // This doesn't actually work because disconnectAndDestroyNetwork only notifies
+        // NetworkStatsService before the underlying network is actually removed. So the underlying
+        // network will only be removed if notifyIfacesChangedForNetworkStats is called again. This
+        // could result in incorrect data usage measurements if the interface used by the
+        // disconnected network is reused by a system component that does not register an agent for
+        // it (e.g., tethering).
+        mCellNetworkAgent.disconnect();
+        waitForIdle();
+        assertNull(mService.getLinkProperties(mCellNetworkAgent.getNetwork()));
+        expectNotifyNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
+                List.of(MOBILE_IFNAME, WIFI_IFNAME));
+
+        // Confirm that we never tell NetworkStatsService that cell is no longer the underlying
+        // network for the VPN...
+        verify(mStatsManager, never()).notifyNetworkStatus(any(List.class),
+                any(List.class), any() /* anyString() doesn't match null */,
+                argThat(infos -> infos.get(0).getUnderlyingInterfaces().size() == 1
+                        && WIFI_IFNAME.equals(infos.get(0).getUnderlyingInterfaces().get(0))));
+        verifyNoMoreInteractions(mStatsManager);
+        reset(mStatsManager);
+
+        // ... but if something else happens that causes notifyIfacesChangedForNetworkStats to be
+        // called again, it does. For example, connect Ethernet, but with a low score, such that it
+        // does not become the default network.
+        mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
+        mEthernetNetworkAgent.setScore(
+                new NetworkScore.Builder().setLegacyInt(30).setExiting(true).build());
+        mEthernetNetworkAgent.connect(false);
+        waitForIdle();
+        verify(mStatsManager).notifyNetworkStatus(any(List.class),
+                any(List.class), any() /* anyString() doesn't match null */,
+                argThat(vpnInfos -> vpnInfos.get(0).getUnderlyingInterfaces().size() == 1
+                        && WIFI_IFNAME.equals(vpnInfos.get(0).getUnderlyingInterfaces().get(0))));
+        mEthernetNetworkAgent.disconnect();
+        waitForIdle();
+        reset(mStatsManager);
+
+        // When a VPN declares no underlying networks (i.e., no connectivity), getAllVpnInfo
+        // does not return the VPN, so CS does not pass it to NetworkStatsService. This causes
+        // NetworkStatsFactory#adjustForTunAnd464Xlat not to attempt any VPN data migration, which
+        // is probably a performance improvement (though it's very unlikely that a VPN would declare
+        // no underlying networks).
+        // Also, for the same reason as above, the active interface passed in is null.
+        mMockVpn.setUnderlyingNetworks(new Network[0]);
+        waitForIdle();
+        expectNotifyNetworkStatus(wifiAndVpn, null);
+        reset(mStatsManager);
+
+        // Specifying only a null underlying network is the same as no networks.
+        mMockVpn.setUnderlyingNetworks(onlyNull);
+        waitForIdle();
+        expectNotifyNetworkStatus(wifiAndVpn, null);
+        reset(mStatsManager);
+
+        // Specifying networks that are all disconnected is the same as specifying no networks.
+        mMockVpn.setUnderlyingNetworks(onlyCell.toArray(new Network[0]));
+        waitForIdle();
+        expectNotifyNetworkStatus(wifiAndVpn, null);
+        reset(mStatsManager);
+
+        // Passing in null again means follow the default network again.
+        mMockVpn.setUnderlyingNetworks(null);
+        waitForIdle();
+        expectNotifyNetworkStatus(wifiAndVpn, WIFI_IFNAME, Process.myUid(), VPN_IFNAME,
+                List.of(WIFI_IFNAME));
+        reset(mStatsManager);
+    }
+
+    @Test
+    public void testBasicDnsConfigurationPushed() throws Exception {
+        setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com");
+
+        // Clear any interactions that occur as a result of CS starting up.
+        reset(mMockDnsResolver);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        waitForIdle();
+        verify(mMockDnsResolver, never()).setResolverConfiguration(any());
+        verifyNoMoreInteractions(mMockDnsResolver);
+
+        final LinkProperties cellLp = new LinkProperties();
+        cellLp.setInterfaceName(MOBILE_IFNAME);
+        // Add IPv4 and IPv6 default routes, because DNS-over-TLS code does
+        // "is-reachable" testing in order to not program netd with unreachable
+        // nameservers that it might try repeated to validate.
+        cellLp.addLinkAddress(new LinkAddress("192.0.2.4/24"));
+        cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("192.0.2.4"),
+                MOBILE_IFNAME));
+        cellLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64"));
+        cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("2001:db8:1::1"),
+                MOBILE_IFNAME));
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        mCellNetworkAgent.connect(false);
+        waitForIdle();
+
+        verify(mMockDnsResolver, times(1)).createNetworkCache(
+                eq(mCellNetworkAgent.getNetwork().netId));
+        // CS tells dnsresolver about the empty DNS config for this network.
+        verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(any());
+        reset(mMockDnsResolver);
+
+        cellLp.addDnsServer(InetAddress.getByName("2001:db8::1"));
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        waitForIdle();
+        verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(
+                mResolverParamsParcelCaptor.capture());
+        ResolverParamsParcel resolvrParams = mResolverParamsParcelCaptor.getValue();
+        assertEquals(1, resolvrParams.servers.length);
+        assertTrue(ArrayUtils.contains(resolvrParams.servers, "2001:db8::1"));
+        // Opportunistic mode.
+        assertTrue(ArrayUtils.contains(resolvrParams.tlsServers, "2001:db8::1"));
+        reset(mMockDnsResolver);
+
+        cellLp.addDnsServer(InetAddress.getByName("192.0.2.1"));
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        waitForIdle();
+        verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(
+                mResolverParamsParcelCaptor.capture());
+        resolvrParams = mResolverParamsParcelCaptor.getValue();
+        assertEquals(2, resolvrParams.servers.length);
+        assertTrue(ArrayUtils.containsAll(resolvrParams.servers,
+                new String[]{"2001:db8::1", "192.0.2.1"}));
+        // Opportunistic mode.
+        assertEquals(2, resolvrParams.tlsServers.length);
+        assertTrue(ArrayUtils.containsAll(resolvrParams.tlsServers,
+                new String[]{"2001:db8::1", "192.0.2.1"}));
+        reset(mMockDnsResolver);
+
+        final String TLS_SPECIFIER = "tls.example.com";
+        final String TLS_SERVER6 = "2001:db8:53::53";
+        final InetAddress[] TLS_IPS = new InetAddress[]{ InetAddress.getByName(TLS_SERVER6) };
+        final String[] TLS_SERVERS = new String[]{ TLS_SERVER6 };
+        mCellNetworkAgent.mNmCallbacks.notifyPrivateDnsConfigResolved(
+                new PrivateDnsConfig(TLS_SPECIFIER, TLS_IPS).toParcel());
+
+        waitForIdle();
+        verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(
+                mResolverParamsParcelCaptor.capture());
+        resolvrParams = mResolverParamsParcelCaptor.getValue();
+        assertEquals(2, resolvrParams.servers.length);
+        assertTrue(ArrayUtils.containsAll(resolvrParams.servers,
+                new String[]{"2001:db8::1", "192.0.2.1"}));
+        reset(mMockDnsResolver);
+    }
+
+    @Test
+    public void testDnsConfigurationTransTypesPushed() throws Exception {
+        // Clear any interactions that occur as a result of CS starting up.
+        reset(mMockDnsResolver);
+
+        final NetworkRequest request = new NetworkRequest.Builder()
+                .clearCapabilities().addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        verify(mMockDnsResolver, times(1)).createNetworkCache(
+                eq(mWiFiNetworkAgent.getNetwork().netId));
+        verify(mMockDnsResolver, times(2)).setResolverConfiguration(
+                mResolverParamsParcelCaptor.capture());
+        final ResolverParamsParcel resolverParams = mResolverParamsParcelCaptor.getValue();
+        assertContainsExactly(resolverParams.transportTypes, TRANSPORT_WIFI);
+        reset(mMockDnsResolver);
+    }
+
+    @Test
+    public void testPrivateDnsNotification() throws Exception {
+        NetworkRequest request = new NetworkRequest.Builder()
+                .clearCapabilities().addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+        // Bring up wifi.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        // Private DNS resolution failed, checking if the notification will be shown or not.
+        mWiFiNetworkAgent.setNetworkInvalid(true /* isStrictMode */);
+        mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid());
+        waitForIdle();
+        // If network validation failed, NetworkMonitor will re-evaluate the network.
+        // ConnectivityService should filter the redundant notification. This part is trying to
+        // simulate that situation and check if ConnectivityService could filter that case.
+        mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid());
+        waitForIdle();
+        verify(mNotificationManager, timeout(TIMEOUT_MS).times(1)).notify(anyString(),
+                eq(NotificationType.PRIVATE_DNS_BROKEN.eventId), any());
+        // If private DNS resolution successful, the PRIVATE_DNS_BROKEN notification shouldn't be
+        // shown.
+        mWiFiNetworkAgent.setNetworkValid(true /* isStrictMode */);
+        mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid());
+        waitForIdle();
+        verify(mNotificationManager, timeout(TIMEOUT_MS).times(1)).cancel(anyString(),
+                eq(NotificationType.PRIVATE_DNS_BROKEN.eventId));
+        // If private DNS resolution failed again, the PRIVATE_DNS_BROKEN notification should be
+        // shown again.
+        mWiFiNetworkAgent.setNetworkInvalid(true /* isStrictMode */);
+        mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid());
+        waitForIdle();
+        verify(mNotificationManager, timeout(TIMEOUT_MS).times(2)).notify(anyString(),
+                eq(NotificationType.PRIVATE_DNS_BROKEN.eventId), any());
+    }
+
+    @Test
+    public void testPrivateDnsSettingsChange() throws Exception {
+        // Clear any interactions that occur as a result of CS starting up.
+        reset(mMockDnsResolver);
+
+        // The default on Android is opportunistic mode ("Automatic").
+        setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com");
+
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.requestNetwork(cellRequest, cellNetworkCallback);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        waitForIdle();
+        // CS tells netd about the empty DNS config for this network.
+        verify(mMockDnsResolver, never()).setResolverConfiguration(any());
+        verifyNoMoreInteractions(mMockDnsResolver);
+
+        final LinkProperties cellLp = new LinkProperties();
+        cellLp.setInterfaceName(MOBILE_IFNAME);
+        // Add IPv4 and IPv6 default routes, because DNS-over-TLS code does
+        // "is-reachable" testing in order to not program netd with unreachable
+        // nameservers that it might try repeated to validate.
+        cellLp.addLinkAddress(new LinkAddress("192.0.2.4/24"));
+        cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("192.0.2.4"),
+                MOBILE_IFNAME));
+        cellLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64"));
+        cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("2001:db8:1::1"),
+                MOBILE_IFNAME));
+        cellLp.addDnsServer(InetAddress.getByName("2001:db8::1"));
+        cellLp.addDnsServer(InetAddress.getByName("192.0.2.1"));
+
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        mCellNetworkAgent.connect(false);
+        waitForIdle();
+        verify(mMockDnsResolver, times(1)).createNetworkCache(
+                eq(mCellNetworkAgent.getNetwork().netId));
+        verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(
+                mResolverParamsParcelCaptor.capture());
+        ResolverParamsParcel resolvrParams = mResolverParamsParcelCaptor.getValue();
+        assertEquals(2, resolvrParams.tlsServers.length);
+        assertTrue(ArrayUtils.containsAll(resolvrParams.tlsServers,
+                new String[] { "2001:db8::1", "192.0.2.1" }));
+        // Opportunistic mode.
+        assertEquals(2, resolvrParams.tlsServers.length);
+        assertTrue(ArrayUtils.containsAll(resolvrParams.tlsServers,
+                new String[] { "2001:db8::1", "192.0.2.1" }));
+        reset(mMockDnsResolver);
+        cellNetworkCallback.expectCallback(CallbackEntry.AVAILABLE, mCellNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED,
+                mCellNetworkAgent);
+        CallbackEntry.LinkPropertiesChanged cbi = cellNetworkCallback.expectCallback(
+                CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackEntry.BLOCKED_STATUS, mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        assertFalse(cbi.getLp().isPrivateDnsActive());
+        assertNull(cbi.getLp().getPrivateDnsServerName());
+
+        setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com");
+        verify(mMockDnsResolver, times(1)).setResolverConfiguration(
+                mResolverParamsParcelCaptor.capture());
+        resolvrParams = mResolverParamsParcelCaptor.getValue();
+        assertEquals(2, resolvrParams.servers.length);
+        assertTrue(ArrayUtils.containsAll(resolvrParams.servers,
+                new String[] { "2001:db8::1", "192.0.2.1" }));
+        reset(mMockDnsResolver);
+        cellNetworkCallback.assertNoCallback();
+
+        setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com");
+        verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(
+                mResolverParamsParcelCaptor.capture());
+        resolvrParams = mResolverParamsParcelCaptor.getValue();
+        assertEquals(2, resolvrParams.servers.length);
+        assertTrue(ArrayUtils.containsAll(resolvrParams.servers,
+                new String[] { "2001:db8::1", "192.0.2.1" }));
+        assertEquals(2, resolvrParams.tlsServers.length);
+        assertTrue(ArrayUtils.containsAll(resolvrParams.tlsServers,
+                new String[] { "2001:db8::1", "192.0.2.1" }));
+        reset(mMockDnsResolver);
+        cellNetworkCallback.assertNoCallback();
+
+        setPrivateDnsSettings(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, "strict.example.com");
+        // Can't test dns configuration for strict mode without properly mocking
+        // out the DNS lookups, but can test that LinkProperties is updated.
+        cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED,
+                mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        assertTrue(cbi.getLp().isPrivateDnsActive());
+        assertEquals("strict.example.com", cbi.getLp().getPrivateDnsServerName());
+    }
+
+    private PrivateDnsValidationEventParcel makePrivateDnsValidationEvent(
+            final int netId, final String ipAddress, final String hostname, final int validation) {
+        final PrivateDnsValidationEventParcel event = new PrivateDnsValidationEventParcel();
+        event.netId = netId;
+        event.ipAddress = ipAddress;
+        event.hostname = hostname;
+        event.validation = validation;
+        return event;
+    }
+
+    @Test
+    public void testLinkPropertiesWithPrivateDnsValidationEvents() throws Exception {
+        // The default on Android is opportunistic mode ("Automatic").
+        setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com");
+
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.requestNetwork(cellRequest, cellNetworkCallback);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        waitForIdle();
+        LinkProperties lp = new LinkProperties();
+        mCellNetworkAgent.sendLinkProperties(lp);
+        mCellNetworkAgent.connect(false);
+        waitForIdle();
+        cellNetworkCallback.expectCallback(CallbackEntry.AVAILABLE, mCellNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED,
+                mCellNetworkAgent);
+        CallbackEntry.LinkPropertiesChanged cbi = cellNetworkCallback.expectCallback(
+                CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
+        cellNetworkCallback.expectCallback(CallbackEntry.BLOCKED_STATUS, mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        assertFalse(cbi.getLp().isPrivateDnsActive());
+        assertNull(cbi.getLp().getPrivateDnsServerName());
+        Set<InetAddress> dnsServers = new HashSet<>();
+        checkDnsServers(cbi.getLp(), dnsServers);
+
+        // Send a validation event for a server that is not part of the current
+        // resolver config. The validation event should be ignored.
+        mService.mResolverUnsolEventCallback.onPrivateDnsValidationEvent(
+                makePrivateDnsValidationEvent(mCellNetworkAgent.getNetwork().netId, "",
+                        "145.100.185.18", VALIDATION_RESULT_SUCCESS));
+        cellNetworkCallback.assertNoCallback();
+
+        // Add a dns server to the LinkProperties.
+        LinkProperties lp2 = new LinkProperties(lp);
+        lp2.addDnsServer(InetAddress.getByName("145.100.185.16"));
+        mCellNetworkAgent.sendLinkProperties(lp2);
+        cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED,
+                mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        assertFalse(cbi.getLp().isPrivateDnsActive());
+        assertNull(cbi.getLp().getPrivateDnsServerName());
+        dnsServers.add(InetAddress.getByName("145.100.185.16"));
+        checkDnsServers(cbi.getLp(), dnsServers);
+
+        // Send a validation event containing a hostname that is not part of
+        // the current resolver config. The validation event should be ignored.
+        mService.mResolverUnsolEventCallback.onPrivateDnsValidationEvent(
+                makePrivateDnsValidationEvent(mCellNetworkAgent.getNetwork().netId,
+                        "145.100.185.16", "hostname", VALIDATION_RESULT_SUCCESS));
+        cellNetworkCallback.assertNoCallback();
+
+        // Send a validation event where validation failed.
+        mService.mResolverUnsolEventCallback.onPrivateDnsValidationEvent(
+                makePrivateDnsValidationEvent(mCellNetworkAgent.getNetwork().netId,
+                        "145.100.185.16", "", VALIDATION_RESULT_FAILURE));
+        cellNetworkCallback.assertNoCallback();
+
+        // Send a validation event where validation succeeded for a server in
+        // the current resolver config. A LinkProperties callback with updated
+        // private dns fields should be sent.
+        mService.mResolverUnsolEventCallback.onPrivateDnsValidationEvent(
+                makePrivateDnsValidationEvent(mCellNetworkAgent.getNetwork().netId,
+                        "145.100.185.16", "", VALIDATION_RESULT_SUCCESS));
+        cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED,
+                mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        assertTrue(cbi.getLp().isPrivateDnsActive());
+        assertNull(cbi.getLp().getPrivateDnsServerName());
+        checkDnsServers(cbi.getLp(), dnsServers);
+
+        // The private dns fields in LinkProperties should be preserved when
+        // the network agent sends unrelated changes.
+        LinkProperties lp3 = new LinkProperties(lp2);
+        lp3.setMtu(1300);
+        mCellNetworkAgent.sendLinkProperties(lp3);
+        cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED,
+                mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        assertTrue(cbi.getLp().isPrivateDnsActive());
+        assertNull(cbi.getLp().getPrivateDnsServerName());
+        checkDnsServers(cbi.getLp(), dnsServers);
+        assertEquals(1300, cbi.getLp().getMtu());
+
+        // Removing the only validated server should affect the private dns
+        // fields in LinkProperties.
+        LinkProperties lp4 = new LinkProperties(lp3);
+        lp4.removeDnsServer(InetAddress.getByName("145.100.185.16"));
+        mCellNetworkAgent.sendLinkProperties(lp4);
+        cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED,
+                mCellNetworkAgent);
+        cellNetworkCallback.assertNoCallback();
+        assertFalse(cbi.getLp().isPrivateDnsActive());
+        assertNull(cbi.getLp().getPrivateDnsServerName());
+        dnsServers.remove(InetAddress.getByName("145.100.185.16"));
+        checkDnsServers(cbi.getLp(), dnsServers);
+        assertEquals(1300, cbi.getLp().getMtu());
+    }
+
+    private void checkDirectlyConnectedRoutes(Object callbackObj,
+            Collection<LinkAddress> linkAddresses, Collection<RouteInfo> otherRoutes) {
+        assertTrue(callbackObj instanceof LinkProperties);
+        LinkProperties lp = (LinkProperties) callbackObj;
+
+        Set<RouteInfo> expectedRoutes = new ArraySet<>();
+        expectedRoutes.addAll(otherRoutes);
+        for (LinkAddress address : linkAddresses) {
+            RouteInfo localRoute = new RouteInfo(address, null, lp.getInterfaceName());
+            // Duplicates in linkAddresses are considered failures
+            assertTrue(expectedRoutes.add(localRoute));
+        }
+        List<RouteInfo> observedRoutes = lp.getRoutes();
+        assertEquals(expectedRoutes.size(), observedRoutes.size());
+        assertTrue(observedRoutes.containsAll(expectedRoutes));
+    }
+
+    private static void checkDnsServers(Object callbackObj, Set<InetAddress> dnsServers) {
+        assertTrue(callbackObj instanceof LinkProperties);
+        LinkProperties lp = (LinkProperties) callbackObj;
+        assertEquals(dnsServers.size(), lp.getDnsServers().size());
+        assertTrue(lp.getDnsServers().containsAll(dnsServers));
+    }
+
+    @Test
+    public void testApplyUnderlyingCapabilities() throws Exception {
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mCellNetworkAgent.connect(false /* validated */);
+        mWiFiNetworkAgent.connect(false /* validated */);
+
+        final NetworkCapabilities cellNc = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .setLinkDownstreamBandwidthKbps(10);
+        final NetworkCapabilities wifiNc = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_WIFI)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_METERED)
+                .addCapability(NET_CAPABILITY_NOT_ROAMING)
+                .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+                .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .setLinkUpstreamBandwidthKbps(20);
+        mCellNetworkAgent.setNetworkCapabilities(cellNc, true /* sendToConnectivityService */);
+        mWiFiNetworkAgent.setNetworkCapabilities(wifiNc, true /* sendToConnectivityService */);
+        waitForIdle();
+
+        final Network mobile = mCellNetworkAgent.getNetwork();
+        final Network wifi = mWiFiNetworkAgent.getNetwork();
+
+        final NetworkCapabilities initialCaps = new NetworkCapabilities();
+        initialCaps.addTransportType(TRANSPORT_VPN);
+        initialCaps.addCapability(NET_CAPABILITY_INTERNET);
+        initialCaps.removeCapability(NET_CAPABILITY_NOT_VPN);
+
+        final NetworkCapabilities withNoUnderlying = new NetworkCapabilities();
+        withNoUnderlying.addCapability(NET_CAPABILITY_INTERNET);
+        withNoUnderlying.addCapability(NET_CAPABILITY_NOT_CONGESTED);
+        withNoUnderlying.addCapability(NET_CAPABILITY_NOT_ROAMING);
+        withNoUnderlying.addCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        withNoUnderlying.addTransportType(TRANSPORT_VPN);
+        withNoUnderlying.removeCapability(NET_CAPABILITY_NOT_VPN);
+
+        final NetworkCapabilities withMobileUnderlying = new NetworkCapabilities(withNoUnderlying);
+        withMobileUnderlying.addTransportType(TRANSPORT_CELLULAR);
+        withMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_ROAMING);
+        withMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        withMobileUnderlying.setLinkDownstreamBandwidthKbps(10);
+
+        final NetworkCapabilities withWifiUnderlying = new NetworkCapabilities(withNoUnderlying);
+        withWifiUnderlying.addTransportType(TRANSPORT_WIFI);
+        withWifiUnderlying.addCapability(NET_CAPABILITY_NOT_METERED);
+        withWifiUnderlying.setLinkUpstreamBandwidthKbps(20);
+
+        final NetworkCapabilities withWifiAndMobileUnderlying =
+                new NetworkCapabilities(withNoUnderlying);
+        withWifiAndMobileUnderlying.addTransportType(TRANSPORT_CELLULAR);
+        withWifiAndMobileUnderlying.addTransportType(TRANSPORT_WIFI);
+        withWifiAndMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_METERED);
+        withWifiAndMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_ROAMING);
+        withWifiAndMobileUnderlying.setLinkDownstreamBandwidthKbps(10);
+        withWifiAndMobileUnderlying.setLinkUpstreamBandwidthKbps(20);
+
+        final NetworkCapabilities initialCapsNotMetered = new NetworkCapabilities(initialCaps);
+        initialCapsNotMetered.addCapability(NET_CAPABILITY_NOT_METERED);
+
+        NetworkCapabilities caps = new NetworkCapabilities(initialCaps);
+        mService.applyUnderlyingCapabilities(new Network[]{}, initialCapsNotMetered, caps);
+        assertEquals(withNoUnderlying, caps);
+
+        caps = new NetworkCapabilities(initialCaps);
+        mService.applyUnderlyingCapabilities(new Network[]{null}, initialCapsNotMetered, caps);
+        assertEquals(withNoUnderlying, caps);
+
+        caps = new NetworkCapabilities(initialCaps);
+        mService.applyUnderlyingCapabilities(new Network[]{mobile}, initialCapsNotMetered, caps);
+        assertEquals(withMobileUnderlying, caps);
+
+        mService.applyUnderlyingCapabilities(new Network[]{wifi}, initialCapsNotMetered, caps);
+        assertEquals(withWifiUnderlying, caps);
+
+        withWifiUnderlying.removeCapability(NET_CAPABILITY_NOT_METERED);
+        caps = new NetworkCapabilities(initialCaps);
+        mService.applyUnderlyingCapabilities(new Network[]{wifi}, initialCaps, caps);
+        assertEquals(withWifiUnderlying, caps);
+
+        caps = new NetworkCapabilities(initialCaps);
+        mService.applyUnderlyingCapabilities(new Network[]{mobile, wifi}, initialCaps, caps);
+        assertEquals(withWifiAndMobileUnderlying, caps);
+
+        withWifiUnderlying.addCapability(NET_CAPABILITY_NOT_METERED);
+        caps = new NetworkCapabilities(initialCaps);
+        mService.applyUnderlyingCapabilities(new Network[]{null, mobile, null, wifi},
+                initialCapsNotMetered, caps);
+        assertEquals(withWifiAndMobileUnderlying, caps);
+
+        caps = new NetworkCapabilities(initialCaps);
+        mService.applyUnderlyingCapabilities(new Network[]{null, mobile, null, wifi},
+                initialCapsNotMetered, caps);
+        assertEquals(withWifiAndMobileUnderlying, caps);
+
+        mService.applyUnderlyingCapabilities(null, initialCapsNotMetered, caps);
+        assertEquals(withWifiUnderlying, caps);
+    }
+
+    @Test
+    public void testVpnConnectDisconnectUnderlyingNetwork() throws Exception {
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        final NetworkRequest request = new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_NOT_VPN).build();
+
+        mCm.registerNetworkCallback(request, callback);
+
+        // Bring up a VPN that specifies an underlying network that does not exist yet.
+        // Note: it's sort of meaningless for a VPN app to declare a network that doesn't exist yet,
+        // (and doing so is difficult without using reflection) but it's good to test that the code
+        // behaves approximately correctly.
+        mMockVpn.establishForMyUid(false, true, false);
+        assertUidRangesUpdatedForMyUid(true);
+        final Network wifiNetwork = new Network(mNetIdManager.peekNextNetId());
+        mMockVpn.setUnderlyingNetworks(new Network[]{wifiNetwork});
+        callback.expectAvailableCallbacksUnvalidated(mMockVpn);
+        assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                .hasTransport(TRANSPORT_VPN));
+        assertFalse(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                .hasTransport(TRANSPORT_WIFI));
+
+        // Make that underlying network connect, and expect to see its capabilities immediately
+        // reflected in the VPN's capabilities.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        assertEquals(wifiNetwork, mWiFiNetworkAgent.getNetwork());
+        mWiFiNetworkAgent.connect(false);
+        // TODO: the callback for the VPN happens before any callbacks are called for the wifi
+        // network that has just connected. There appear to be two issues here:
+        // 1. The VPN code will accept an underlying network as soon as getNetworkCapabilities() for
+        //    it returns non-null (which happens very early, during handleRegisterNetworkAgent).
+        //    This is not correct because that that point the network is not connected and cannot
+        //    pass any traffic.
+        // 2. When a network connects, updateNetworkInfo propagates underlying network capabilities
+        //    before rematching networks.
+        // Given that this scenario can't really happen, this is probably fine for now.
+        callback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, mMockVpn);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                .hasTransport(TRANSPORT_VPN));
+        assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                .hasTransport(TRANSPORT_WIFI));
+
+        // Disconnect the network, and expect to see the VPN capabilities change accordingly.
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        callback.expectCapabilitiesThat(mMockVpn, (nc) ->
+                nc.getTransportTypes().length == 1 && nc.hasTransport(TRANSPORT_VPN));
+
+        mMockVpn.disconnect();
+        mCm.unregisterNetworkCallback(callback);
+    }
+
+    private void assertGetNetworkInfoOfGetActiveNetworkIsConnected(boolean expectedConnectivity) {
+        // What Chromium used to do before https://chromium-review.googlesource.com/2605304
+        assertEquals("Unexpected result for getActiveNetworkInfo(getActiveNetwork())",
+                expectedConnectivity, mCm.getNetworkInfo(mCm.getActiveNetwork()).isConnected());
+    }
+
+    @Test
+    public void testVpnUnderlyingNetworkSuspended() throws Exception {
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(callback);
+
+        // Connect a VPN.
+        mMockVpn.establishForMyUid(false /* validated */, true /* hasInternet */,
+                false /* isStrictMode */);
+        callback.expectAvailableCallbacksUnvalidated(mMockVpn);
+
+        // Connect cellular data.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(false /* validated */);
+        callback.expectCapabilitiesThat(mMockVpn,
+                nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                        && nc.hasTransport(TRANSPORT_CELLULAR));
+        callback.assertNoCallback();
+
+        assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                .hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertGetNetworkInfoOfGetActiveNetworkIsConnected(true);
+
+        // Suspend the cellular network and expect the VPN to be suspended.
+        mCellNetworkAgent.suspend();
+        callback.expectCapabilitiesThat(mMockVpn,
+                nc -> !nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                        && nc.hasTransport(TRANSPORT_CELLULAR));
+        callback.expectCallback(CallbackEntry.SUSPENDED, mMockVpn);
+        callback.assertNoCallback();
+
+        assertFalse(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                .hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.SUSPENDED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_VPN, DetailedState.SUSPENDED);
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.SUSPENDED);
+        // VPN's main underlying network is suspended, so no connectivity.
+        assertGetNetworkInfoOfGetActiveNetworkIsConnected(false);
+
+        // Switch to another network. The VPN should no longer be suspended.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false /* validated */);
+        callback.expectCapabilitiesThat(mMockVpn,
+                nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                        && nc.hasTransport(TRANSPORT_WIFI));
+        callback.expectCallback(CallbackEntry.RESUMED, mMockVpn);
+        callback.assertNoCallback();
+
+        assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                .hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        assertGetNetworkInfoOfGetActiveNetworkIsConnected(true);
+
+        // Unsuspend cellular and then switch back to it. The VPN remains not suspended.
+        mCellNetworkAgent.resume();
+        callback.assertNoCallback();
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCapabilitiesThat(mMockVpn,
+                nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                        && nc.hasTransport(TRANSPORT_CELLULAR));
+        // Spurious double callback?
+        callback.expectCapabilitiesThat(mMockVpn,
+                nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                        && nc.hasTransport(TRANSPORT_CELLULAR));
+        callback.assertNoCallback();
+
+        assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                .hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertGetNetworkInfoOfGetActiveNetworkIsConnected(true);
+
+        // Suspend cellular and expect no connectivity.
+        mCellNetworkAgent.suspend();
+        callback.expectCapabilitiesThat(mMockVpn,
+                nc -> !nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                        && nc.hasTransport(TRANSPORT_CELLULAR));
+        callback.expectCallback(CallbackEntry.SUSPENDED, mMockVpn);
+        callback.assertNoCallback();
+
+        assertFalse(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                .hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.SUSPENDED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_VPN, DetailedState.SUSPENDED);
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.SUSPENDED);
+        assertGetNetworkInfoOfGetActiveNetworkIsConnected(false);
+
+        // Resume cellular and expect that connectivity comes back.
+        mCellNetworkAgent.resume();
+        callback.expectCapabilitiesThat(mMockVpn,
+                nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                        && nc.hasTransport(TRANSPORT_CELLULAR));
+        callback.expectCallback(CallbackEntry.RESUMED, mMockVpn);
+        callback.assertNoCallback();
+
+        assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork())
+                .hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertGetNetworkInfoOfGetActiveNetworkIsConnected(true);
+    }
+
+    @Test
+    public void testVpnNetworkActive() throws Exception {
+        // NETWORK_SETTINGS is necessary to call registerSystemDefaultNetworkCallback.
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+        final int uid = Process.myUid();
+
+        final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback();
+        final TestNetworkCallback genericNotVpnNetworkCallback = new TestNetworkCallback();
+        final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback();
+        final TestNetworkCallback vpnNetworkCallback = new TestNetworkCallback();
+        final TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback();
+        final NetworkRequest genericNotVpnRequest = new NetworkRequest.Builder().build();
+        final NetworkRequest genericRequest = new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_NOT_VPN).build();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        final NetworkRequest vpnNetworkRequest = new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_NOT_VPN)
+                .addTransportType(TRANSPORT_VPN).build();
+        mCm.registerNetworkCallback(genericRequest, genericNetworkCallback);
+        mCm.registerNetworkCallback(genericNotVpnRequest, genericNotVpnNetworkCallback);
+        mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
+        mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback);
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+        mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback,
+                new Handler(ConnectivityThread.getInstanceLooper()));
+        defaultCallback.assertNoCallback();
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+
+        genericNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        genericNotVpnNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        wifiNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        vpnNetworkCallback.assertNoCallback();
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        final Set<UidRange> ranges = uidRangesForUids(uid);
+        mMockVpn.registerAgent(ranges);
+        mMockVpn.setUnderlyingNetworks(new Network[0]);
+
+        // VPN networks do not satisfy the default request and are automatically validated
+        // by NetworkMonitor
+        assertFalse(NetworkMonitorUtils.isValidationRequired(
+                mMockVpn.getAgent().getNetworkCapabilities()));
+        mMockVpn.getAgent().setNetworkValid(false /* isStrictMode */);
+
+        mMockVpn.connect(false);
+
+        genericNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        genericNotVpnNetworkCallback.assertNoCallback();
+        wifiNetworkCallback.assertNoCallback();
+        vpnNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        systemDefaultCallback.assertNoCallback();
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+        assertEquals(mWiFiNetworkAgent.getNetwork(),
+                systemDefaultCallback.getLastAvailableNetwork());
+
+        ranges.clear();
+        mMockVpn.setUids(ranges);
+
+        genericNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn);
+        genericNotVpnNetworkCallback.assertNoCallback();
+        wifiNetworkCallback.assertNoCallback();
+        vpnNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn);
+
+        // TODO : The default network callback should actually get a LOST call here (also see the
+        // comment below for AVAILABLE). This is because ConnectivityService does not look at UID
+        // ranges at all when determining whether a network should be rematched. In practice, VPNs
+        // can't currently update their UIDs without disconnecting, so this does not matter too
+        // much, but that is the reason the test here has to check for an update to the
+        // capabilities instead of the expected LOST then AVAILABLE.
+        defaultCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, mMockVpn);
+        systemDefaultCallback.assertNoCallback();
+
+        ranges.add(new UidRange(uid, uid));
+        mMockVpn.setUids(ranges);
+
+        genericNetworkCallback.expectAvailableCallbacksValidated(mMockVpn);
+        genericNotVpnNetworkCallback.assertNoCallback();
+        wifiNetworkCallback.assertNoCallback();
+        vpnNetworkCallback.expectAvailableCallbacksValidated(mMockVpn);
+        // TODO : Here like above, AVAILABLE would be correct, but because this can't actually
+        // happen outside of the test, ConnectivityService does not rematch callbacks.
+        defaultCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, mMockVpn);
+        systemDefaultCallback.assertNoCallback();
+
+        mWiFiNetworkAgent.disconnect();
+
+        genericNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        genericNotVpnNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        wifiNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        vpnNetworkCallback.assertNoCallback();
+        defaultCallback.assertNoCallback();
+        systemDefaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+
+        mMockVpn.disconnect();
+
+        genericNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn);
+        genericNotVpnNetworkCallback.assertNoCallback();
+        wifiNetworkCallback.assertNoCallback();
+        vpnNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn);
+        defaultCallback.expectCallback(CallbackEntry.LOST, mMockVpn);
+        systemDefaultCallback.assertNoCallback();
+        assertEquals(null, mCm.getActiveNetwork());
+
+        mCm.unregisterNetworkCallback(genericNetworkCallback);
+        mCm.unregisterNetworkCallback(wifiNetworkCallback);
+        mCm.unregisterNetworkCallback(vpnNetworkCallback);
+        mCm.unregisterNetworkCallback(defaultCallback);
+        mCm.unregisterNetworkCallback(systemDefaultCallback);
+    }
+
+    @Test
+    public void testVpnWithoutInternet() throws Exception {
+        final int uid = Process.myUid();
+
+        final TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+
+        defaultCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */,
+                false /* isStrictMode */);
+        assertUidRangesUpdatedForMyUid(true);
+
+        defaultCallback.assertNoCallback();
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        mMockVpn.disconnect();
+        defaultCallback.assertNoCallback();
+
+        mCm.unregisterNetworkCallback(defaultCallback);
+    }
+
+    @Test
+    public void testVpnWithInternet() throws Exception {
+        final int uid = Process.myUid();
+
+        final TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+
+        defaultCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        mMockVpn.establishForMyUid(true /* validated */, true /* hasInternet */,
+                false /* isStrictMode */);
+        assertUidRangesUpdatedForMyUid(true);
+
+        defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork());
+
+        mMockVpn.disconnect();
+        defaultCallback.expectCallback(CallbackEntry.LOST, mMockVpn);
+        defaultCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
+
+        mCm.unregisterNetworkCallback(defaultCallback);
+    }
+
+    @Test
+    public void testVpnUnvalidated() throws Exception {
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(callback);
+
+        // Bring up Ethernet.
+        mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
+        mEthernetNetworkAgent.connect(true);
+        callback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent);
+        callback.assertNoCallback();
+
+        // Bring up a VPN that has the INTERNET capability, initially unvalidated.
+        mMockVpn.establishForMyUid(false /* validated */, true /* hasInternet */,
+                false /* isStrictMode */);
+        assertUidRangesUpdatedForMyUid(true);
+
+        // Even though the VPN is unvalidated, it becomes the default network for our app.
+        callback.expectAvailableCallbacksUnvalidated(mMockVpn);
+        callback.assertNoCallback();
+
+        assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork());
+
+        NetworkCapabilities nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
+        assertFalse(nc.hasCapability(NET_CAPABILITY_VALIDATED));
+        assertTrue(nc.hasCapability(NET_CAPABILITY_INTERNET));
+
+        assertFalse(NetworkMonitorUtils.isValidationRequired(
+                mMockVpn.getAgent().getNetworkCapabilities()));
+        assertTrue(NetworkMonitorUtils.isPrivateDnsValidationRequired(
+                mMockVpn.getAgent().getNetworkCapabilities()));
+
+        // Pretend that the VPN network validates.
+        mMockVpn.getAgent().setNetworkValid(false /* isStrictMode */);
+        mMockVpn.getAgent().mNetworkMonitor.forceReevaluation(Process.myUid());
+        // Expect to see the validated capability, but no other changes, because the VPN is already
+        // the default network for the app.
+        callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mMockVpn);
+        callback.assertNoCallback();
+
+        mMockVpn.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mMockVpn);
+        callback.expectAvailableCallbacksValidated(mEthernetNetworkAgent);
+    }
+
+    @Test
+    public void testVpnStartsWithUnderlyingCaps() throws Exception {
+        final int uid = Process.myUid();
+
+        final TestNetworkCallback vpnNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest vpnNetworkRequest = new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_NOT_VPN)
+                .addTransportType(TRANSPORT_VPN)
+                .build();
+        mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback);
+        vpnNetworkCallback.assertNoCallback();
+
+        // Connect cell. It will become the default network, and in the absence of setting
+        // underlying networks explicitly it will become the sole underlying network for the vpn.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        mCellNetworkAgent.connect(true);
+
+        mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */,
+                false /* isStrictMode */);
+        assertUidRangesUpdatedForMyUid(true);
+
+        vpnNetworkCallback.expectAvailableCallbacks(mMockVpn.getNetwork(),
+                false /* suspended */, false /* validated */, false /* blocked */, TIMEOUT_MS);
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn.getNetwork(), TIMEOUT_MS,
+                nc -> nc.hasCapability(NET_CAPABILITY_VALIDATED));
+
+        final NetworkCapabilities nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
+        assertTrue(nc.hasTransport(TRANSPORT_VPN));
+        assertTrue(nc.hasTransport(TRANSPORT_CELLULAR));
+        assertFalse(nc.hasTransport(TRANSPORT_WIFI));
+        assertTrue(nc.hasCapability(NET_CAPABILITY_VALIDATED));
+        assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_METERED));
+        assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+
+        assertVpnTransportInfo(nc, VpnManager.TYPE_VPN_SERVICE);
+    }
+
+    private void assertDefaultNetworkCapabilities(int userId, NetworkAgentWrapper... networks) {
+        final NetworkCapabilities[] defaultCaps = mService.getDefaultNetworkCapabilitiesForUser(
+                userId, "com.android.calling.package", "com.test");
+        final String defaultCapsString = Arrays.toString(defaultCaps);
+        assertEquals(defaultCapsString, defaultCaps.length, networks.length);
+        final Set<NetworkCapabilities> defaultCapsSet = new ArraySet<>(defaultCaps);
+        for (NetworkAgentWrapper network : networks) {
+            final NetworkCapabilities nc = mCm.getNetworkCapabilities(network.getNetwork());
+            final String msg = "Did not find " + nc + " in " + Arrays.toString(defaultCaps);
+            assertTrue(msg, defaultCapsSet.contains(nc));
+        }
+    }
+
+    @Test
+    public void testVpnSetUnderlyingNetworks() throws Exception {
+        final TestNetworkCallback vpnNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest vpnNetworkRequest = new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_NOT_VPN)
+                .addTransportType(TRANSPORT_VPN)
+                .build();
+        NetworkCapabilities nc;
+        mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback);
+        vpnNetworkCallback.assertNoCallback();
+
+        mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */,
+                false /* isStrictMode */);
+        assertUidRangesUpdatedForMyUid(true);
+
+        vpnNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
+        assertTrue(nc.hasTransport(TRANSPORT_VPN));
+        assertFalse(nc.hasTransport(TRANSPORT_CELLULAR));
+        assertFalse(nc.hasTransport(TRANSPORT_WIFI));
+        // For safety reasons a VPN without underlying networks is considered metered.
+        assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_METERED));
+        // A VPN without underlying networks is not suspended.
+        assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        assertVpnTransportInfo(nc, VpnManager.TYPE_VPN_SERVICE);
+
+        final int userId = UserHandle.getUserId(Process.myUid());
+        assertDefaultNetworkCapabilities(userId /* no networks */);
+
+        // Connect cell and use it as an underlying network.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        mCellNetworkAgent.connect(true);
+
+        mMockVpn.setUnderlyingNetworks(
+                new Network[] { mCellNetworkAgent.getNetwork() });
+
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)
+                && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        assertDefaultNetworkCapabilities(userId, mCellNetworkAgent);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        mWiFiNetworkAgent.connect(true);
+
+        mMockVpn.setUnderlyingNetworks(
+                new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() });
+
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)
+                && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent);
+
+        // Don't disconnect, but note the VPN is not using wifi any more.
+        mMockVpn.setUnderlyingNetworks(
+                new Network[] { mCellNetworkAgent.getNetwork() });
+
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)
+                && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        // The return value of getDefaultNetworkCapabilitiesForUser always includes the default
+        // network (wifi) as well as the underlying networks (cell).
+        assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent);
+
+        // Remove NOT_SUSPENDED from the only network and observe VPN is now suspended.
+        mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        vpnNetworkCallback.expectCallback(CallbackEntry.SUSPENDED, mMockVpn);
+
+        // Add NOT_SUSPENDED again and observe VPN is no longer suspended.
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)
+                && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        vpnNetworkCallback.expectCallback(CallbackEntry.RESUMED, mMockVpn);
+
+        // Use Wifi but not cell. Note the VPN is now unmetered and not suspended.
+        mMockVpn.setUnderlyingNetworks(
+                new Network[] { mWiFiNetworkAgent.getNetwork() });
+
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && !caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI)
+                && caps.hasCapability(NET_CAPABILITY_NOT_METERED)
+                && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        assertDefaultNetworkCapabilities(userId, mWiFiNetworkAgent);
+
+        // Use both again.
+        mMockVpn.setUnderlyingNetworks(
+                new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() });
+
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)
+                && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent);
+
+        // Cell is suspended again. As WiFi is not, this should not cause a callback.
+        mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_SUSPENDED);
+        vpnNetworkCallback.assertNoCallback();
+
+        // Stop using WiFi. The VPN is suspended again.
+        mMockVpn.setUnderlyingNetworks(
+                new Network[] { mCellNetworkAgent.getNetwork() });
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasTransport(TRANSPORT_CELLULAR)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        vpnNetworkCallback.expectCallback(CallbackEntry.SUSPENDED, mMockVpn);
+        assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent);
+
+        // Use both again.
+        mMockVpn.setUnderlyingNetworks(
+                new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() });
+
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)
+                && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED));
+        vpnNetworkCallback.expectCallback(CallbackEntry.RESUMED, mMockVpn);
+        assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent);
+
+        // Disconnect cell. Receive update without even removing the dead network from the
+        // underlying networks – it's dead anyway. Not metered any more.
+        mCellNetworkAgent.disconnect();
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && !caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI)
+                && caps.hasCapability(NET_CAPABILITY_NOT_METERED));
+        assertDefaultNetworkCapabilities(userId, mWiFiNetworkAgent);
+
+        // Disconnect wifi too. No underlying networks means this is now metered.
+        mWiFiNetworkAgent.disconnect();
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && !caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_METERED));
+        // When a network disconnects, the callbacks are fired before all state is updated, so for a
+        // short time, synchronous calls will behave as if the network is still connected. Wait for
+        // things to settle.
+        waitForIdle();
+        assertDefaultNetworkCapabilities(userId /* no networks */);
+
+        mMockVpn.disconnect();
+    }
+
+    @Test
+    public void testNullUnderlyingNetworks() throws Exception {
+        final int uid = Process.myUid();
+
+        final TestNetworkCallback vpnNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest vpnNetworkRequest = new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_NOT_VPN)
+                .addTransportType(TRANSPORT_VPN)
+                .build();
+        NetworkCapabilities nc;
+        mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback);
+        vpnNetworkCallback.assertNoCallback();
+
+        mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */,
+                false /* isStrictMode */);
+        assertUidRangesUpdatedForMyUid(true);
+
+        vpnNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
+        assertTrue(nc.hasTransport(TRANSPORT_VPN));
+        assertFalse(nc.hasTransport(TRANSPORT_CELLULAR));
+        assertFalse(nc.hasTransport(TRANSPORT_WIFI));
+        // By default, VPN is set to track default network (i.e. its underlying networks is null).
+        // In case of no default network, VPN is considered metered.
+        assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_METERED));
+        assertVpnTransportInfo(nc, VpnManager.TYPE_VPN_SERVICE);
+
+        // Connect to Cell; Cell is the default network.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_METERED));
+
+        // Connect to WiFi; WiFi is the new default.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mWiFiNetworkAgent.connect(true);
+
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && !caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI)
+                && caps.hasCapability(NET_CAPABILITY_NOT_METERED));
+
+        // Disconnect Cell. The default network did not change, so there shouldn't be any changes in
+        // the capabilities.
+        mCellNetworkAgent.disconnect();
+
+        // Disconnect wifi too. Now we have no default network.
+        mWiFiNetworkAgent.disconnect();
+
+        vpnNetworkCallback.expectCapabilitiesThat(mMockVpn,
+                (caps) -> caps.hasTransport(TRANSPORT_VPN)
+                && !caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI)
+                && !caps.hasCapability(NET_CAPABILITY_NOT_METERED));
+
+        mMockVpn.disconnect();
+    }
+
+    @Test
+    public void testRestrictedProfileAffectsVpnUidRanges() throws Exception {
+        // NETWORK_SETTINGS is necessary to see the UID ranges in NetworkCapabilities.
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+        final NetworkRequest request = new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_NOT_VPN)
+                .build();
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+
+        // Bring up a VPN
+        mMockVpn.establishForMyUid();
+        assertUidRangesUpdatedForMyUid(true);
+        callback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        callback.assertNoCallback();
+
+        final int uid = Process.myUid();
+        NetworkCapabilities nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
+        assertNotNull("nc=" + nc, nc.getUids());
+        assertEquals(nc.getUids(), UidRange.toIntRanges(uidRangesForUids(uid)));
+        assertVpnTransportInfo(nc, VpnManager.TYPE_VPN_SERVICE);
+
+        // Set an underlying network and expect to see the VPN transports change.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        callback.expectCapabilitiesThat(mMockVpn, (caps)
+                -> caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasTransport(TRANSPORT_WIFI));
+        callback.expectCapabilitiesThat(mWiFiNetworkAgent, (caps)
+                -> caps.hasCapability(NET_CAPABILITY_VALIDATED));
+
+        when(mPackageManager.getPackageUidAsUser(ALWAYS_ON_PACKAGE, RESTRICTED_USER))
+                .thenReturn(UserHandle.getUid(RESTRICTED_USER, VPN_UID));
+
+        final Intent addedIntent = new Intent(ACTION_USER_ADDED);
+        addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER));
+        addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER);
+
+        // Send a USER_ADDED broadcast for it.
+        processBroadcast(addedIntent);
+
+        // Expect that the VPN UID ranges contain both |uid| and the UID range for the newly-added
+        // restricted user.
+        final UidRange rRange = UidRange.createForUser(UserHandle.of(RESTRICTED_USER));
+        final Range<Integer> restrictUidRange = new Range<Integer>(rRange.start, rRange.stop);
+        final Range<Integer> singleUidRange = new Range<Integer>(uid, uid);
+        callback.expectCapabilitiesThat(mMockVpn, (caps)
+                -> caps.getUids().size() == 2
+                && caps.getUids().contains(singleUidRange)
+                && caps.getUids().contains(restrictUidRange)
+                && caps.hasTransport(TRANSPORT_VPN)
+                && caps.hasTransport(TRANSPORT_WIFI));
+
+        // Change the VPN's capabilities somehow (specifically, disconnect wifi).
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        callback.expectCapabilitiesThat(mMockVpn, (caps)
+                -> caps.getUids().size() == 2
+                && caps.getUids().contains(singleUidRange)
+                && caps.getUids().contains(restrictUidRange)
+                && caps.hasTransport(TRANSPORT_VPN)
+                && !caps.hasTransport(TRANSPORT_WIFI));
+
+        // Send a USER_REMOVED broadcast and expect to lose the UID range for the restricted user.
+        final Intent removedIntent = new Intent(ACTION_USER_REMOVED);
+        removedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER));
+        removedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER);
+        processBroadcast(removedIntent);
+
+        // Expect that the VPN gains the UID range for the restricted user, and that the capability
+        // change made just before that (i.e., loss of TRANSPORT_WIFI) is preserved.
+        callback.expectCapabilitiesThat(mMockVpn, (caps)
+                -> caps.getUids().size() == 1
+                && caps.getUids().contains(singleUidRange)
+                && caps.hasTransport(TRANSPORT_VPN)
+                && !caps.hasTransport(TRANSPORT_WIFI));
+    }
+
+    @Test
+    public void testLockdownVpnWithRestrictedProfiles() throws Exception {
+        // For ConnectivityService#setAlwaysOnVpnPackage.
+        mServiceContext.setPermission(
+                Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED);
+        // For call Vpn#setAlwaysOnPackage.
+        mServiceContext.setPermission(
+                Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
+        // Necessary to see the UID ranges in NetworkCapabilities.
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+        final NetworkRequest request = new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_NOT_VPN)
+                .build();
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+
+        final int uid = Process.myUid();
+
+        // Connect wifi and check that UIDs in the main and restricted profiles have network access.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true /* validated */);
+        final int restrictedUid = UserHandle.getUid(RESTRICTED_USER, 42 /* appId */);
+        assertNotNull(mCm.getActiveNetworkForUid(uid));
+        assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
+
+        // Enable always-on VPN lockdown. The main user loses network access because no VPN is up.
+        final ArrayList<String> allowList = new ArrayList<>();
+        mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, ALWAYS_ON_PACKAGE,
+                true /* lockdown */, allowList);
+        waitForIdle();
+        assertNull(mCm.getActiveNetworkForUid(uid));
+        // This is arguably overspecified: a UID that is not running doesn't have an active network.
+        // But it's useful to check that non-default users do not lose network access, and to prove
+        // that the loss of connectivity below is indeed due to the restricted profile coming up.
+        assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
+
+        // Start the restricted profile, and check that the UID within it loses network access.
+        when(mPackageManager.getPackageUidAsUser(ALWAYS_ON_PACKAGE, RESTRICTED_USER))
+                .thenReturn(UserHandle.getUid(RESTRICTED_USER, VPN_UID));
+        when(mUserManager.getAliveUsers()).thenReturn(Arrays.asList(PRIMARY_USER_INFO,
+                RESTRICTED_USER_INFO));
+        // TODO: check that VPN app within restricted profile still has access, etc.
+        final Intent addedIntent = new Intent(ACTION_USER_ADDED);
+        addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER));
+        addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER);
+        processBroadcast(addedIntent);
+        assertNull(mCm.getActiveNetworkForUid(uid));
+        assertNull(mCm.getActiveNetworkForUid(restrictedUid));
+
+        // Stop the restricted profile, and check that the UID within it has network access again.
+        when(mUserManager.getAliveUsers()).thenReturn(Arrays.asList(PRIMARY_USER_INFO));
+
+        // Send a USER_REMOVED broadcast and expect to lose the UID range for the restricted user.
+        final Intent removedIntent = new Intent(ACTION_USER_REMOVED);
+        removedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER));
+        removedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER);
+        processBroadcast(removedIntent);
+        assertNull(mCm.getActiveNetworkForUid(uid));
+        assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
+
+        mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, null, false /* lockdown */,
+                allowList);
+        waitForIdle();
+    }
+
+    @Test
+    public void testIsActiveNetworkMeteredOverWifi() throws Exception {
+        // Returns true by default when no network is available.
+        assertTrue(mCm.isActiveNetworkMetered());
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mWiFiNetworkAgent.connect(true);
+        waitForIdle();
+
+        assertFalse(mCm.isActiveNetworkMetered());
+    }
+
+    @Test
+    public void testIsActiveNetworkMeteredOverCell() throws Exception {
+        // Returns true by default when no network is available.
+        assertTrue(mCm.isActiveNetworkMetered());
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED);
+        mCellNetworkAgent.connect(true);
+        waitForIdle();
+
+        assertTrue(mCm.isActiveNetworkMetered());
+    }
+
+    @Test
+    public void testIsActiveNetworkMeteredOverVpnTrackingPlatformDefault() throws Exception {
+        // Returns true by default when no network is available.
+        assertTrue(mCm.isActiveNetworkMetered());
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED);
+        mCellNetworkAgent.connect(true);
+        waitForIdle();
+        assertTrue(mCm.isActiveNetworkMetered());
+
+        // Connect VPN network. By default it is using current default network (Cell).
+        mMockVpn.establishForMyUid();
+        assertUidRangesUpdatedForMyUid(true);
+
+        // Ensure VPN is now the active network.
+        assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork());
+
+        // Expect VPN to be metered.
+        assertTrue(mCm.isActiveNetworkMetered());
+
+        // Connect WiFi.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mWiFiNetworkAgent.connect(true);
+        waitForIdle();
+        // VPN should still be the active network.
+        assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork());
+
+        // Expect VPN to be unmetered as it should now be using WiFi (new default).
+        assertFalse(mCm.isActiveNetworkMetered());
+
+        // Disconnecting Cell should not affect VPN's meteredness.
+        mCellNetworkAgent.disconnect();
+        waitForIdle();
+
+        assertFalse(mCm.isActiveNetworkMetered());
+
+        // Disconnect WiFi; Now there is no platform default network.
+        mWiFiNetworkAgent.disconnect();
+        waitForIdle();
+
+        // VPN without any underlying networks is treated as metered.
+        assertTrue(mCm.isActiveNetworkMetered());
+
+        mMockVpn.disconnect();
+    }
+
+   @Test
+   public void testIsActiveNetworkMeteredOverVpnSpecifyingUnderlyingNetworks() throws Exception {
+        // Returns true by default when no network is available.
+        assertTrue(mCm.isActiveNetworkMetered());
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED);
+        mCellNetworkAgent.connect(true);
+        waitForIdle();
+        assertTrue(mCm.isActiveNetworkMetered());
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mWiFiNetworkAgent.connect(true);
+        waitForIdle();
+        assertFalse(mCm.isActiveNetworkMetered());
+
+        // Connect VPN network.
+        mMockVpn.establishForMyUid();
+        assertUidRangesUpdatedForMyUid(true);
+
+        // Ensure VPN is now the active network.
+        assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork());
+        // VPN is using Cell
+        mMockVpn.setUnderlyingNetworks(
+                new Network[] { mCellNetworkAgent.getNetwork() });
+        waitForIdle();
+
+        // Expect VPN to be metered.
+        assertTrue(mCm.isActiveNetworkMetered());
+
+        // VPN is now using WiFi
+        mMockVpn.setUnderlyingNetworks(
+                new Network[] { mWiFiNetworkAgent.getNetwork() });
+        waitForIdle();
+
+        // Expect VPN to be unmetered
+        assertFalse(mCm.isActiveNetworkMetered());
+
+        // VPN is using Cell | WiFi.
+        mMockVpn.setUnderlyingNetworks(
+                new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() });
+        waitForIdle();
+
+        // Expect VPN to be metered.
+        assertTrue(mCm.isActiveNetworkMetered());
+
+        // VPN is using WiFi | Cell.
+        mMockVpn.setUnderlyingNetworks(
+                new Network[] { mWiFiNetworkAgent.getNetwork(), mCellNetworkAgent.getNetwork() });
+        waitForIdle();
+
+        // Order should not matter and VPN should still be metered.
+        assertTrue(mCm.isActiveNetworkMetered());
+
+        // VPN is not using any underlying networks.
+        mMockVpn.setUnderlyingNetworks(new Network[0]);
+        waitForIdle();
+
+        // VPN without underlying networks is treated as metered.
+        assertTrue(mCm.isActiveNetworkMetered());
+
+        mMockVpn.disconnect();
+    }
+
+    @Test
+    public void testIsActiveNetworkMeteredOverAlwaysMeteredVpn() throws Exception {
+        // Returns true by default when no network is available.
+        assertTrue(mCm.isActiveNetworkMetered());
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mWiFiNetworkAgent.connect(true);
+        waitForIdle();
+        assertFalse(mCm.isActiveNetworkMetered());
+
+        // Connect VPN network.
+        mMockVpn.registerAgent(true /* isAlwaysMetered */, uidRangesForUids(Process.myUid()),
+                new LinkProperties());
+        mMockVpn.connect(true);
+        waitForIdle();
+        assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork());
+
+        // VPN is tracking current platform default (WiFi).
+        mMockVpn.setUnderlyingNetworks(null);
+        waitForIdle();
+
+        // Despite VPN using WiFi (which is unmetered), VPN itself is marked as always metered.
+        assertTrue(mCm.isActiveNetworkMetered());
+
+
+        // VPN explicitly declares WiFi as its underlying network.
+        mMockVpn.setUnderlyingNetworks(
+                new Network[] { mWiFiNetworkAgent.getNetwork() });
+        waitForIdle();
+
+        // Doesn't really matter whether VPN declares its underlying networks explicitly.
+        assertTrue(mCm.isActiveNetworkMetered());
+
+        // With WiFi lost, VPN is basically without any underlying networks. And in that case it is
+        // anyways suppose to be metered.
+        mWiFiNetworkAgent.disconnect();
+        waitForIdle();
+
+        assertTrue(mCm.isActiveNetworkMetered());
+
+        mMockVpn.disconnect();
+    }
+
+    private class DetailedBlockedStatusCallback extends TestNetworkCallback {
+        public void expectAvailableThenValidatedCallbacks(HasNetwork n, int blockedStatus) {
+            super.expectAvailableThenValidatedCallbacks(n.getNetwork(), blockedStatus, TIMEOUT_MS);
+        }
+        public void expectBlockedStatusCallback(HasNetwork n, int blockedStatus) {
+            // This doesn't work:
+            // super.expectBlockedStatusCallback(blockedStatus, n.getNetwork());
+            super.expectBlockedStatusCallback(blockedStatus, n.getNetwork(), TIMEOUT_MS);
+        }
+        public void onBlockedStatusChanged(Network network, int blockedReasons) {
+            getHistory().add(new CallbackEntry.BlockedStatusInt(network, blockedReasons));
+        }
+    }
+
+    @Test
+    public void testNetworkBlockedStatus() throws Exception {
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .build();
+        mCm.registerNetworkCallback(cellRequest, cellNetworkCallback);
+        final DetailedBlockedStatusCallback detailedCallback = new DetailedBlockedStatusCallback();
+        mCm.registerNetworkCallback(cellRequest, detailedCallback);
+
+        mockUidNetworkingBlocked();
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        detailedCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent,
+                BLOCKED_REASON_NONE);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertExtraInfoFromCmPresent(mCellNetworkAgent);
+
+        setBlockedReasonChanged(BLOCKED_REASON_BATTERY_SAVER);
+        cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent);
+        detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent,
+                BLOCKED_REASON_BATTERY_SAVER);
+        assertNull(mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertExtraInfoFromCmBlocked(mCellNetworkAgent);
+
+        // If blocked state does not change but blocked reason does, the boolean callback is called.
+        // TODO: investigate de-duplicating.
+        setBlockedReasonChanged(BLOCKED_METERED_REASON_USER_RESTRICTED);
+        cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent);
+        detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent,
+                BLOCKED_METERED_REASON_USER_RESTRICTED);
+
+        setBlockedReasonChanged(BLOCKED_REASON_NONE);
+        cellNetworkCallback.expectBlockedStatusCallback(false, mCellNetworkAgent);
+        detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, BLOCKED_REASON_NONE);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertExtraInfoFromCmPresent(mCellNetworkAgent);
+
+        setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER);
+        cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent);
+        detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent,
+                BLOCKED_METERED_REASON_DATA_SAVER);
+        assertNull(mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertExtraInfoFromCmBlocked(mCellNetworkAgent);
+
+        // Restrict the network based on UID rule and NOT_METERED capability change.
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        cellNetworkCallback.expectCapabilitiesWith(NET_CAPABILITY_NOT_METERED, mCellNetworkAgent);
+        cellNetworkCallback.expectBlockedStatusCallback(false, mCellNetworkAgent);
+        detailedCallback.expectCapabilitiesWith(NET_CAPABILITY_NOT_METERED, mCellNetworkAgent);
+        detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, BLOCKED_REASON_NONE);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertExtraInfoFromCmPresent(mCellNetworkAgent);
+
+        mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED);
+        cellNetworkCallback.expectCapabilitiesWithout(NET_CAPABILITY_NOT_METERED,
+                mCellNetworkAgent);
+        cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent);
+        detailedCallback.expectCapabilitiesWithout(NET_CAPABILITY_NOT_METERED,
+                mCellNetworkAgent);
+        detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent,
+                BLOCKED_METERED_REASON_DATA_SAVER);
+        assertNull(mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertExtraInfoFromCmBlocked(mCellNetworkAgent);
+
+        setBlockedReasonChanged(BLOCKED_REASON_NONE);
+        cellNetworkCallback.expectBlockedStatusCallback(false, mCellNetworkAgent);
+        detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, BLOCKED_REASON_NONE);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertExtraInfoFromCmPresent(mCellNetworkAgent);
+
+        setBlockedReasonChanged(BLOCKED_REASON_NONE);
+        cellNetworkCallback.assertNoCallback();
+        detailedCallback.assertNoCallback();
+
+        // Restrict background data. Networking is not blocked because the network is unmetered.
+        setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER);
+        cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent);
+        detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent,
+                BLOCKED_METERED_REASON_DATA_SAVER);
+        assertNull(mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertExtraInfoFromCmBlocked(mCellNetworkAgent);
+        setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER);
+        cellNetworkCallback.assertNoCallback();
+
+        setBlockedReasonChanged(BLOCKED_REASON_NONE);
+        cellNetworkCallback.expectBlockedStatusCallback(false, mCellNetworkAgent);
+        detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, BLOCKED_REASON_NONE);
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertExtraInfoFromCmPresent(mCellNetworkAgent);
+
+        setBlockedReasonChanged(BLOCKED_REASON_NONE);
+        cellNetworkCallback.assertNoCallback();
+        detailedCallback.assertNoCallback();
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertExtraInfoFromCmPresent(mCellNetworkAgent);
+
+        mCm.unregisterNetworkCallback(cellNetworkCallback);
+    }
+
+    @Test
+    public void testNetworkBlockedStatusBeforeAndAfterConnect() throws Exception {
+        final TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+        mockUidNetworkingBlocked();
+
+        // No Networkcallbacks invoked before any network is active.
+        setBlockedReasonChanged(BLOCKED_REASON_BATTERY_SAVER);
+        setBlockedReasonChanged(BLOCKED_REASON_NONE);
+        setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER);
+        defaultCallback.assertNoCallback();
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent);
+        defaultCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mCellNetworkAgent);
+
+        // Allow to use the network after switching to NOT_METERED network.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mWiFiNetworkAgent.connect(true);
+        defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+
+        // Switch to METERED network. Restrict the use of the network.
+        mWiFiNetworkAgent.disconnect();
+        defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        defaultCallback.expectAvailableCallbacksValidatedAndBlocked(mCellNetworkAgent);
+
+        // Network becomes NOT_METERED.
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        defaultCallback.expectCapabilitiesWith(NET_CAPABILITY_NOT_METERED, mCellNetworkAgent);
+        defaultCallback.expectBlockedStatusCallback(false, mCellNetworkAgent);
+
+        // Verify there's no Networkcallbacks invoked after data saver on/off.
+        setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER);
+        setBlockedReasonChanged(BLOCKED_REASON_NONE);
+        defaultCallback.assertNoCallback();
+
+        mCellNetworkAgent.disconnect();
+        defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        defaultCallback.assertNoCallback();
+
+        mCm.unregisterNetworkCallback(defaultCallback);
+    }
+
+    private void expectNetworkRejectNonSecureVpn(InOrder inOrder, boolean add,
+            UidRangeParcel... expected) throws Exception {
+        inOrder.verify(mMockNetd).networkRejectNonSecureVpn(eq(add), aryEq(expected));
+    }
+
+    private void checkNetworkInfo(NetworkInfo ni, int type, DetailedState state) {
+        assertNotNull(ni);
+        assertEquals(type, ni.getType());
+        assertEquals(ConnectivityManager.getNetworkTypeName(type), state, ni.getDetailedState());
+        if (state == DetailedState.CONNECTED || state == DetailedState.SUSPENDED) {
+            assertNotNull(ni.getExtraInfo());
+        } else {
+            // Technically speaking, a network that's in CONNECTING state will generally have a
+            // non-null extraInfo. This doesn't actually happen in this test because it never calls
+            // a legacy API while a network is connecting. When a network is in CONNECTING state
+            // because of legacy lockdown VPN, its extraInfo is always null.
+            assertNull(ni.getExtraInfo());
+        }
+    }
+
+    private void assertActiveNetworkInfo(int type, DetailedState state) {
+        checkNetworkInfo(mCm.getActiveNetworkInfo(), type, state);
+    }
+    private void assertNetworkInfo(int type, DetailedState state) {
+        checkNetworkInfo(mCm.getNetworkInfo(type), type, state);
+    }
+
+    private void assertExtraInfoFromCm(TestNetworkAgentWrapper network, boolean present) {
+        final NetworkInfo niForNetwork = mCm.getNetworkInfo(network.getNetwork());
+        final NetworkInfo niForType = mCm.getNetworkInfo(network.getLegacyType());
+        if (present) {
+            assertEquals(network.getExtraInfo(), niForNetwork.getExtraInfo());
+            assertEquals(network.getExtraInfo(), niForType.getExtraInfo());
+        } else {
+            assertNull(niForNetwork.getExtraInfo());
+            assertNull(niForType.getExtraInfo());
+        }
+    }
+
+    private void assertExtraInfoFromCmBlocked(TestNetworkAgentWrapper network) {
+        assertExtraInfoFromCm(network, false);
+    }
+
+    private void assertExtraInfoFromCmPresent(TestNetworkAgentWrapper network) {
+        assertExtraInfoFromCm(network, true);
+    }
+
+    // Checks that each of the |agents| receive a blocked status change callback with the specified
+    // |blocked| value, in any order. This is needed because when an event affects multiple
+    // networks, ConnectivityService does not guarantee the order in which callbacks are fired.
+    private void assertBlockedCallbackInAnyOrder(TestNetworkCallback callback, boolean blocked,
+            TestNetworkAgentWrapper... agents) {
+        final List<Network> expectedNetworks = Arrays.asList(agents).stream()
+                .map((agent) -> agent.getNetwork())
+                .collect(Collectors.toList());
+
+        // Expect exactly one blocked callback for each agent.
+        for (int i = 0; i < agents.length; i++) {
+            CallbackEntry e = callback.expectCallbackThat(TIMEOUT_MS, (c) ->
+                    c instanceof CallbackEntry.BlockedStatus
+                            && ((CallbackEntry.BlockedStatus) c).getBlocked() == blocked);
+            Network network = e.getNetwork();
+            assertTrue("Received unexpected blocked callback for network " + network,
+                    expectedNetworks.remove(network));
+        }
+    }
+
+    @Test
+    public void testNetworkBlockedStatusAlwaysOnVpn() throws Exception {
+        mServiceContext.setPermission(
+                Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED);
+        mServiceContext.setPermission(
+                Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        final NetworkRequest request = new NetworkRequest.Builder()
+                .removeCapability(NET_CAPABILITY_NOT_VPN)
+                .build();
+        mCm.registerNetworkCallback(request, callback);
+
+        final TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+
+        final TestNetworkCallback vpnUidCallback = new TestNetworkCallback();
+        final NetworkRequest vpnUidRequest = new NetworkRequest.Builder().build();
+        registerNetworkCallbackAsUid(vpnUidRequest, vpnUidCallback, VPN_UID);
+
+        final TestNetworkCallback vpnUidDefaultCallback = new TestNetworkCallback();
+        registerDefaultNetworkCallbackAsUid(vpnUidDefaultCallback, VPN_UID);
+
+        final TestNetworkCallback vpnDefaultCallbackAsUid = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallbackForUid(VPN_UID, vpnDefaultCallbackAsUid,
+                new Handler(ConnectivityThread.getInstanceLooper()));
+
+        final int uid = Process.myUid();
+        final int userId = UserHandle.getUserId(uid);
+        final ArrayList<String> allowList = new ArrayList<>();
+        mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */,
+                allowList);
+        waitForIdle();
+
+        UidRangeParcel firstHalf = new UidRangeParcel(1, VPN_UID - 1);
+        UidRangeParcel secondHalf = new UidRangeParcel(VPN_UID + 1, 99999);
+        InOrder inOrder = inOrder(mMockNetd);
+        expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf);
+
+        // Connect a network when lockdown is active, expect to see it blocked.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false /* validated */);
+        callback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiNetworkAgent);
+        defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiNetworkAgent);
+        vpnUidCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        vpnUidDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        vpnDefaultCallbackAsUid.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
+        assertNull(mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+        // Mobile is BLOCKED even though it's not actually connected.
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+
+        // Disable lockdown, expect to see the network unblocked.
+        mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList);
+        callback.expectBlockedStatusCallback(false, mWiFiNetworkAgent);
+        defaultCallback.expectBlockedStatusCallback(false, mWiFiNetworkAgent);
+        vpnUidCallback.assertNoCallback();
+        vpnUidDefaultCallback.assertNoCallback();
+        vpnDefaultCallbackAsUid.assertNoCallback();
+        expectNetworkRejectNonSecureVpn(inOrder, false, firstHalf, secondHalf);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+
+        // Add our UID to the allowlist and re-enable lockdown, expect network is not blocked.
+        allowList.add(TEST_PACKAGE_NAME);
+        mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */,
+                allowList);
+        callback.assertNoCallback();
+        defaultCallback.assertNoCallback();
+        vpnUidCallback.assertNoCallback();
+        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);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+
+        // Connect a new network, expect it to be unblocked.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(false /* validated */);
+        callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        defaultCallback.assertNoCallback();
+        vpnUidCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        vpnUidDefaultCallback.assertNoCallback();
+        vpnDefaultCallbackAsUid.assertNoCallback();
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        // Cellular is DISCONNECTED because it's not the default and there are no requests for it.
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+
+        // Disable lockdown, remove our UID from the allowlist, and re-enable lockdown.
+        // Everything should now be blocked.
+        mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList);
+        waitForIdle();
+        expectNetworkRejectNonSecureVpn(inOrder, false, piece1, piece2, piece3);
+        allowList.clear();
+        mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */,
+                allowList);
+        waitForIdle();
+        expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf);
+        defaultCallback.expectBlockedStatusCallback(true, mWiFiNetworkAgent);
+        assertBlockedCallbackInAnyOrder(callback, true, mWiFiNetworkAgent, mCellNetworkAgent);
+        vpnUidCallback.assertNoCallback();
+        vpnUidDefaultCallback.assertNoCallback();
+        vpnDefaultCallbackAsUid.assertNoCallback();
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
+        assertNull(mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+
+        // Disable lockdown. Everything is unblocked.
+        mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList);
+        defaultCallback.expectBlockedStatusCallback(false, mWiFiNetworkAgent);
+        assertBlockedCallbackInAnyOrder(callback, false, mWiFiNetworkAgent, mCellNetworkAgent);
+        vpnUidCallback.assertNoCallback();
+        vpnUidDefaultCallback.assertNoCallback();
+        vpnDefaultCallbackAsUid.assertNoCallback();
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+
+        // Enable and disable an always-on VPN package without lockdown. Expect no changes.
+        reset(mMockNetd);
+        mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, false /* lockdown */,
+                allowList);
+        inOrder.verify(mMockNetd, never()).networkRejectNonSecureVpn(anyBoolean(), any());
+        callback.assertNoCallback();
+        defaultCallback.assertNoCallback();
+        vpnUidCallback.assertNoCallback();
+        vpnUidDefaultCallback.assertNoCallback();
+        vpnDefaultCallbackAsUid.assertNoCallback();
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+
+        mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList);
+        inOrder.verify(mMockNetd, never()).networkRejectNonSecureVpn(anyBoolean(), any());
+        callback.assertNoCallback();
+        defaultCallback.assertNoCallback();
+        vpnUidCallback.assertNoCallback();
+        vpnUidDefaultCallback.assertNoCallback();
+        vpnDefaultCallbackAsUid.assertNoCallback();
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+
+        // Enable lockdown and connect a VPN. The VPN is not blocked.
+        mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */,
+                allowList);
+        defaultCallback.expectBlockedStatusCallback(true, mWiFiNetworkAgent);
+        assertBlockedCallbackInAnyOrder(callback, true, mWiFiNetworkAgent, mCellNetworkAgent);
+        vpnUidCallback.assertNoCallback();
+        vpnUidDefaultCallback.assertNoCallback();
+        vpnDefaultCallbackAsUid.assertNoCallback();
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
+        assertNull(mCm.getActiveNetwork());
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+
+        mMockVpn.establishForMyUid();
+        assertUidRangesUpdatedForMyUid(true);
+        defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        vpnUidCallback.assertNoCallback();  // vpnUidCallback has NOT_VPN capability.
+        vpnUidDefaultCallback.assertNoCallback();  // VPN does not apply to VPN_UID
+        vpnDefaultCallbackAsUid.assertNoCallback();
+        assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork());
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+
+        mMockVpn.disconnect();
+        defaultCallback.expectCallback(CallbackEntry.LOST, mMockVpn);
+        defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiNetworkAgent);
+        vpnUidCallback.assertNoCallback();
+        vpnUidDefaultCallback.assertNoCallback();
+        vpnDefaultCallbackAsUid.assertNoCallback();
+        assertNull(mCm.getActiveNetwork());
+
+        mCm.unregisterNetworkCallback(callback);
+        mCm.unregisterNetworkCallback(defaultCallback);
+        mCm.unregisterNetworkCallback(vpnUidCallback);
+        mCm.unregisterNetworkCallback(vpnUidDefaultCallback);
+        mCm.unregisterNetworkCallback(vpnDefaultCallbackAsUid);
+    }
+
+    private void setupLegacyLockdownVpn() {
+        final String profileName = "testVpnProfile";
+        final byte[] profileTag = profileName.getBytes(StandardCharsets.UTF_8);
+        when(mVpnProfileStore.get(Credentials.LOCKDOWN_VPN)).thenReturn(profileTag);
+
+        final VpnProfile profile = new VpnProfile(profileName);
+        profile.name = "My VPN";
+        profile.server = "192.0.2.1";
+        profile.dnsServers = "8.8.8.8";
+        profile.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK;
+        final byte[] encodedProfile = profile.encode();
+        when(mVpnProfileStore.get(Credentials.VPN + profileName)).thenReturn(encodedProfile);
+    }
+
+    private void establishLegacyLockdownVpn(Network underlying) throws Exception {
+        // The legacy lockdown VPN only supports userId 0, and must have an underlying network.
+        assertNotNull(underlying);
+        mMockVpn.setVpnType(VpnManager.TYPE_VPN_LEGACY);
+        // The legacy lockdown VPN only supports userId 0.
+        final Set<UidRange> ranges = Collections.singleton(PRIMARY_UIDRANGE);
+        mMockVpn.registerAgent(ranges);
+        mMockVpn.setUnderlyingNetworks(new Network[]{underlying});
+        mMockVpn.connect(true);
+    }
+
+    @Test
+    public void testLegacyLockdownVpn() throws Exception {
+        mServiceContext.setPermission(
+                Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
+        // For LockdownVpnTracker to call registerSystemDefaultNetworkCallback.
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+        final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+
+        final TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+
+        final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback();
+        mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback,
+                new Handler(ConnectivityThread.getInstanceLooper()));
+
+        // Pretend lockdown VPN was configured.
+        setupLegacyLockdownVpn();
+
+        // LockdownVpnTracker disables the Vpn teardown code and enables lockdown.
+        // Check the VPN's state before it does so.
+        assertTrue(mMockVpn.getEnableTeardown());
+        assertFalse(mMockVpn.getLockdown());
+
+        // Send a USER_UNLOCKED broadcast so CS starts LockdownVpnTracker.
+        final int userId = UserHandle.getUserId(Process.myUid());
+        final Intent addedIntent = new Intent(ACTION_USER_UNLOCKED);
+        addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId));
+        addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
+        processBroadcast(addedIntent);
+
+        // Lockdown VPN disables teardown and enables lockdown.
+        assertFalse(mMockVpn.getEnableTeardown());
+        assertTrue(mMockVpn.getLockdown());
+
+        // Bring up a network.
+        // Expect nothing to happen because the network does not have an IPv4 default route: legacy
+        // VPN only supports IPv4.
+        final LinkProperties cellLp = new LinkProperties();
+        cellLp.setInterfaceName("rmnet0");
+        cellLp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+        cellLp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, "rmnet0"));
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+        mCellNetworkAgent.connect(false /* validated */);
+        callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent);
+        defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent);
+        systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent);
+        waitForIdle();
+        assertNull(mMockVpn.getAgent());
+
+        // Add an IPv4 address. Ideally the VPN should start, but it doesn't because nothing calls
+        // LockdownVpnTracker#handleStateChangedLocked. This is a bug.
+        // TODO: consider fixing this.
+        cellLp.addLinkAddress(new LinkAddress("192.0.2.2/25"));
+        cellLp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"), null, "rmnet0"));
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        callback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
+        systemDefaultCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED,
+                mCellNetworkAgent);
+        waitForIdle();
+        assertNull(mMockVpn.getAgent());
+
+        // Disconnect, then try again with a network that supports IPv4 at connection time.
+        // Expect lockdown VPN to come up.
+        ExpectedBroadcast b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        mCellNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        systemDefaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        b1.expectBroadcast();
+
+        // When lockdown VPN is active, the NetworkInfo state in CONNECTIVITY_ACTION is overwritten
+        // with the state of the VPN network. So expect a CONNECTING broadcast.
+        b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTING);
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+        mCellNetworkAgent.connect(false /* validated */);
+        callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent);
+        defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent);
+        systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent);
+        b1.expectBroadcast();
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED);
+        assertExtraInfoFromCmBlocked(mCellNetworkAgent);
+
+        // TODO: it would be nice if we could simply rely on the production code here, and have
+        // LockdownVpnTracker start the VPN, have the VPN code register its NetworkAgent with
+        // ConnectivityService, etc. That would require duplicating a fair bit of code from the
+        // Vpn tests around how to mock out LegacyVpnRunner. But even if we did that, this does not
+        // work for at least two reasons:
+        // 1. In this test, calling registerNetworkAgent does not actually result in an agent being
+        //    registered. This is because nothing calls onNetworkMonitorCreated, which is what
+        //    actually ends up causing handleRegisterNetworkAgent to be called. Code in this test
+        //    that wants to register an agent must use TestNetworkAgentWrapper.
+        // 2. Even if we exposed Vpn#agentConnect to the test, and made MockVpn#agentConnect call
+        //    the TestNetworkAgentWrapper code, this would deadlock because the
+        //    TestNetworkAgentWrapper code cannot be called on the handler thread since it calls
+        //    waitForIdle().
+        mMockVpn.expectStartLegacyVpnRunner();
+        b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
+        ExpectedBroadcast b2 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED);
+        establishLegacyLockdownVpn(mCellNetworkAgent.getNetwork());
+        callback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        systemDefaultCallback.assertNoCallback();
+        NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
+        b1.expectBroadcast();
+        b2.expectBroadcast();
+        assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+        assertExtraInfoFromCmPresent(mCellNetworkAgent);
+        assertTrue(vpnNc.hasTransport(TRANSPORT_VPN));
+        assertTrue(vpnNc.hasTransport(TRANSPORT_CELLULAR));
+        assertFalse(vpnNc.hasTransport(TRANSPORT_WIFI));
+        assertFalse(vpnNc.hasCapability(NET_CAPABILITY_NOT_METERED));
+        assertVpnTransportInfo(vpnNc, VpnManager.TYPE_VPN_LEGACY);
+
+        // Switch default network from cell to wifi. Expect VPN to disconnect and reconnect.
+        final LinkProperties wifiLp = new LinkProperties();
+        wifiLp.setInterfaceName("wlan0");
+        wifiLp.addLinkAddress(new LinkAddress("192.0.2.163/25"));
+        wifiLp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"), null, "wlan0"));
+        final NetworkCapabilities wifiNc = new NetworkCapabilities();
+        wifiNc.addTransportType(TRANSPORT_WIFI);
+        wifiNc.addCapability(NET_CAPABILITY_NOT_METERED);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp, wifiNc);
+
+        b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        // Wifi is CONNECTING because the VPN isn't up yet.
+        b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTING);
+        ExpectedBroadcast b3 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+        mWiFiNetworkAgent.connect(false /* validated */);
+        b1.expectBroadcast();
+        b2.expectBroadcast();
+        b3.expectBroadcast();
+        mMockVpn.expectStopVpnRunnerPrivileged();
+        mMockVpn.expectStartLegacyVpnRunner();
+
+        // TODO: why is wifi not blocked? Is it because when this callback is sent, the VPN is still
+        // connected, so the network is not considered blocked by the lockdown UID ranges? But the
+        // fact that a VPN is connected should only result in the VPN itself being unblocked, not
+        // any other network. Bug in isUidBlockedByVpn?
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        callback.expectCallback(CallbackEntry.LOST, mMockVpn);
+        defaultCallback.expectCallback(CallbackEntry.LOST, mMockVpn);
+        defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiNetworkAgent);
+        systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+
+        // While the VPN is reconnecting on the new network, everything is blocked.
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+        assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED);
+        assertExtraInfoFromCmBlocked(mWiFiNetworkAgent);
+
+        // The VPN comes up again on wifi.
+        b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
+        b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
+        establishLegacyLockdownVpn(mWiFiNetworkAgent.getNetwork());
+        callback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
+        systemDefaultCallback.assertNoCallback();
+        b1.expectBroadcast();
+        b2.expectBroadcast();
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+        assertExtraInfoFromCmPresent(mWiFiNetworkAgent);
+        vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
+        assertTrue(vpnNc.hasTransport(TRANSPORT_VPN));
+        assertTrue(vpnNc.hasTransport(TRANSPORT_WIFI));
+        assertFalse(vpnNc.hasTransport(TRANSPORT_CELLULAR));
+        assertTrue(vpnNc.hasCapability(NET_CAPABILITY_NOT_METERED));
+
+        // Disconnect cell. Nothing much happens since it's not the default network.
+        mCellNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        defaultCallback.assertNoCallback();
+        systemDefaultCallback.assertNoCallback();
+
+        assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
+        assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+        assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+        assertExtraInfoFromCmPresent(mWiFiNetworkAgent);
+
+        b1 = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED);
+        b2 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        systemDefaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        b1.expectBroadcast();
+        callback.expectCapabilitiesThat(mMockVpn, nc -> !nc.hasTransport(TRANSPORT_WIFI));
+        mMockVpn.expectStopVpnRunnerPrivileged();
+        callback.expectCallback(CallbackEntry.LOST, mMockVpn);
+        b2.expectBroadcast();
+    }
+
+    /**
+     * Test mutable and requestable network capabilities such as
+     * {@link NetworkCapabilities#NET_CAPABILITY_TRUSTED} and
+     * {@link NetworkCapabilities#NET_CAPABILITY_NOT_VCN_MANAGED}. Verify that the
+     * {@code ConnectivityService} re-assign the networks accordingly.
+     */
+    @Test
+    public final void testLoseMutableAndRequestableCaps() throws Exception {
+        final int[] testCaps = new int [] {
+                NET_CAPABILITY_TRUSTED,
+                NET_CAPABILITY_NOT_VCN_MANAGED
+        };
+        for (final int testCap : testCaps) {
+            // Create requests with and without the testing capability.
+            final TestNetworkCallback callbackWithCap = new TestNetworkCallback();
+            final TestNetworkCallback callbackWithoutCap = new TestNetworkCallback();
+            mCm.requestNetwork(new NetworkRequest.Builder().addCapability(testCap).build(),
+                    callbackWithCap);
+            mCm.requestNetwork(new NetworkRequest.Builder().removeCapability(testCap).build(),
+                    callbackWithoutCap);
+
+            // Setup networks with testing capability and verify the default network changes.
+            mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+            mCellNetworkAgent.addCapability(testCap);
+            mCellNetworkAgent.connect(true);
+            callbackWithCap.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+            callbackWithoutCap.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+            verify(mMockNetd).networkSetDefault(eq(mCellNetworkAgent.getNetwork().netId));
+            reset(mMockNetd);
+
+            mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+            mWiFiNetworkAgent.addCapability(testCap);
+            mWiFiNetworkAgent.connect(true);
+            callbackWithCap.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+            callbackWithoutCap.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+            verify(mMockNetd).networkSetDefault(eq(mWiFiNetworkAgent.getNetwork().netId));
+            reset(mMockNetd);
+
+            // Remove the testing capability on wifi, verify the callback and default network
+            // changes back to cellular.
+            mWiFiNetworkAgent.removeCapability(testCap);
+            callbackWithCap.expectAvailableCallbacksValidated(mCellNetworkAgent);
+            callbackWithoutCap.expectCapabilitiesWithout(testCap, mWiFiNetworkAgent);
+            verify(mMockNetd).networkSetDefault(eq(mCellNetworkAgent.getNetwork().netId));
+            reset(mMockNetd);
+
+            mCellNetworkAgent.removeCapability(testCap);
+            callbackWithCap.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+            callbackWithoutCap.assertNoCallback();
+            verify(mMockNetd).networkClearDefault();
+
+            mCm.unregisterNetworkCallback(callbackWithCap);
+            mCm.unregisterNetworkCallback(callbackWithoutCap);
+        }
+    }
+
+    @Test
+    public final void testBatteryStatsNetworkType() throws Exception {
+        final LinkProperties cellLp = new LinkProperties();
+        cellLp.setInterfaceName("cell0");
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+        mCellNetworkAgent.connect(true);
+        waitForIdle();
+        verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext,
+                cellLp.getInterfaceName(),
+                new int[] { TRANSPORT_CELLULAR });
+
+        final LinkProperties wifiLp = new LinkProperties();
+        wifiLp.setInterfaceName("wifi0");
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+        mWiFiNetworkAgent.connect(true);
+        waitForIdle();
+        verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext,
+                wifiLp.getInterfaceName(),
+                new int[] { TRANSPORT_WIFI });
+
+        mCellNetworkAgent.disconnect();
+        mWiFiNetworkAgent.disconnect();
+
+        cellLp.setInterfaceName("wifi0");
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+        mCellNetworkAgent.connect(true);
+        waitForIdle();
+        verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext,
+                cellLp.getInterfaceName(),
+                new int[] { TRANSPORT_CELLULAR });
+        mCellNetworkAgent.disconnect();
+    }
+
+    /**
+     * Make simulated InterfaceConfigParcel for Nat464Xlat to query clat lower layer info.
+     */
+    private InterfaceConfigurationParcel getClatInterfaceConfigParcel(LinkAddress la) {
+        final InterfaceConfigurationParcel cfg = new InterfaceConfigurationParcel();
+        cfg.hwAddr = "11:22:33:44:55:66";
+        cfg.ipv4Addr = la.getAddress().getHostAddress();
+        cfg.prefixLength = la.getPrefixLength();
+        return cfg;
+    }
+
+    /**
+     * Make expected stack link properties, copied from Nat464Xlat.
+     */
+    private LinkProperties makeClatLinkProperties(LinkAddress la) {
+        LinkAddress clatAddress = la;
+        LinkProperties stacked = new LinkProperties();
+        stacked.setInterfaceName(CLAT_MOBILE_IFNAME);
+        RouteInfo ipv4Default = new RouteInfo(
+                new LinkAddress(Inet4Address.ANY, 0),
+                clatAddress.getAddress(), CLAT_MOBILE_IFNAME);
+        stacked.addRoute(ipv4Default);
+        stacked.addLinkAddress(clatAddress);
+        return stacked;
+    }
+
+    private Nat64PrefixEventParcel makeNat64PrefixEvent(final int netId, final int prefixOperation,
+            final String prefixAddress, final int prefixLength) {
+        final Nat64PrefixEventParcel event = new Nat64PrefixEventParcel();
+        event.netId = netId;
+        event.prefixOperation = prefixOperation;
+        event.prefixAddress = prefixAddress;
+        event.prefixLength = prefixLength;
+        return event;
+    }
+
+    @Test
+    public void testStackedLinkProperties() throws Exception {
+        final LinkAddress myIpv4 = new LinkAddress("1.2.3.4/24");
+        final LinkAddress myIpv6 = new LinkAddress("2001:db8:1::1/64");
+        final String kNat64PrefixString = "2001:db8:64:64:64:64::";
+        final IpPrefix kNat64Prefix = new IpPrefix(InetAddress.getByName(kNat64PrefixString), 96);
+        final String kOtherNat64PrefixString = "64:ff9b::";
+        final IpPrefix kOtherNat64Prefix = new IpPrefix(
+                InetAddress.getByName(kOtherNat64PrefixString), 96);
+        final RouteInfo ipv6Default =
+                new RouteInfo((IpPrefix) null, myIpv6.getAddress(), MOBILE_IFNAME);
+        final RouteInfo ipv6Subnet = new RouteInfo(myIpv6, null, MOBILE_IFNAME);
+        final RouteInfo ipv4Subnet = new RouteInfo(myIpv4, null, MOBILE_IFNAME);
+        final RouteInfo stackedDefault =
+                new RouteInfo((IpPrefix) null, myIpv4.getAddress(), CLAT_MOBILE_IFNAME);
+
+        final NetworkRequest networkRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(networkRequest, networkCallback);
+
+        // Prepare ipv6 only link properties.
+        final LinkProperties cellLp = new LinkProperties();
+        cellLp.setInterfaceName(MOBILE_IFNAME);
+        cellLp.addLinkAddress(myIpv6);
+        cellLp.addRoute(ipv6Default);
+        cellLp.addRoute(ipv6Subnet);
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+        reset(mMockDnsResolver);
+        reset(mMockNetd);
+
+        // Connect with ipv6 link properties. Expect prefix discovery to be started.
+        mCellNetworkAgent.connect(true);
+        int cellNetId = mCellNetworkAgent.getNetwork().netId;
+        waitForIdle();
+
+        verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(cellNetId,
+                INetd.PERMISSION_NONE));
+        assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default);
+        verify(mMockDnsResolver, times(1)).createNetworkCache(eq(cellNetId));
+        verify(mMockNetd, times(1)).networkAddInterface(cellNetId, MOBILE_IFNAME);
+        verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext,
+                cellLp.getInterfaceName(),
+                new int[] { TRANSPORT_CELLULAR });
+
+        networkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        verify(mMockDnsResolver, times(1)).startPrefix64Discovery(cellNetId);
+
+        // Switching default network updates TCP buffer sizes.
+        verifyTcpBufferSizeChange(ConnectivityService.DEFAULT_TCP_BUFFER_SIZES);
+        // Add an IPv4 address. Expect prefix discovery to be stopped. Netd doesn't tell us that
+        // the NAT64 prefix was removed because one was never discovered.
+        cellLp.addLinkAddress(myIpv4);
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
+        assertRoutesAdded(cellNetId, ipv4Subnet);
+        verify(mMockDnsResolver, times(1)).stopPrefix64Discovery(cellNetId);
+        verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(any());
+
+        // Make sure BatteryStats was not told about any v4- interfaces, as none should have
+        // come online yet.
+        waitForIdle();
+        verify(mDeps, never())
+                .reportNetworkInterfaceForTransports(eq(mServiceContext), startsWith("v4-"), any());
+
+        verifyNoMoreInteractions(mMockNetd);
+        verifyNoMoreInteractions(mMockDnsResolver);
+        reset(mMockNetd);
+        reset(mMockDnsResolver);
+        when(mMockNetd.interfaceGetCfg(CLAT_MOBILE_IFNAME))
+                .thenReturn(getClatInterfaceConfigParcel(myIpv4));
+
+        // Remove IPv4 address. Expect prefix discovery to be started again.
+        cellLp.removeLinkAddress(myIpv4);
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
+        verify(mMockDnsResolver, times(1)).startPrefix64Discovery(cellNetId);
+        assertRoutesRemoved(cellNetId, ipv4Subnet);
+
+        // When NAT64 prefix discovery succeeds, LinkProperties are updated and clatd is started.
+        Nat464Xlat clat = getNat464Xlat(mCellNetworkAgent);
+        assertNull(mCm.getLinkProperties(mCellNetworkAgent.getNetwork()).getNat64Prefix());
+        mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
+                makeNat64PrefixEvent(cellNetId, PREFIX_OPERATION_ADDED, kNat64PrefixString, 96));
+        LinkProperties lpBeforeClat = networkCallback.expectCallback(
+                CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent).getLp();
+        assertEquals(0, lpBeforeClat.getStackedLinks().size());
+        assertEquals(kNat64Prefix, lpBeforeClat.getNat64Prefix());
+        verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
+
+        // Clat iface comes up. Expect stacked link to be added.
+        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
+        networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
+        List<LinkProperties> stackedLps = mCm.getLinkProperties(mCellNetworkAgent.getNetwork())
+                .getStackedLinks();
+        assertEquals(makeClatLinkProperties(myIpv4), stackedLps.get(0));
+        assertRoutesAdded(cellNetId, stackedDefault);
+        verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
+        // Change trivial linkproperties and see if stacked link is preserved.
+        cellLp.addDnsServer(InetAddress.getByName("8.8.8.8"));
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
+
+        List<LinkProperties> stackedLpsAfterChange =
+                mCm.getLinkProperties(mCellNetworkAgent.getNetwork()).getStackedLinks();
+        assertNotEquals(stackedLpsAfterChange, Collections.EMPTY_LIST);
+        assertEquals(makeClatLinkProperties(myIpv4), stackedLpsAfterChange.get(0));
+
+        verify(mMockDnsResolver, times(1)).setResolverConfiguration(
+                mResolverParamsParcelCaptor.capture());
+        ResolverParamsParcel resolvrParams = mResolverParamsParcelCaptor.getValue();
+        assertEquals(1, resolvrParams.servers.length);
+        assertTrue(ArrayUtils.contains(resolvrParams.servers, "8.8.8.8"));
+
+        for (final LinkProperties stackedLp : stackedLpsAfterChange) {
+            verify(mDeps).reportNetworkInterfaceForTransports(
+                    mServiceContext, stackedLp.getInterfaceName(),
+                    new int[] { TRANSPORT_CELLULAR });
+        }
+        reset(mMockNetd);
+        when(mMockNetd.interfaceGetCfg(CLAT_MOBILE_IFNAME))
+                .thenReturn(getClatInterfaceConfigParcel(myIpv4));
+        // Change the NAT64 prefix without first removing it.
+        // Expect clatd to be stopped and started with the new prefix.
+        mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent(
+                cellNetId, PREFIX_OPERATION_ADDED, kOtherNat64PrefixString, 96));
+        networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
+                (lp) -> lp.getStackedLinks().size() == 0);
+        verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
+        assertRoutesRemoved(cellNetId, stackedDefault);
+        verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
+
+        verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kOtherNat64Prefix.toString());
+        networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
+                (lp) -> lp.getNat64Prefix().equals(kOtherNat64Prefix));
+        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
+        networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
+                (lp) -> lp.getStackedLinks().size() == 1);
+        assertRoutesAdded(cellNetId, stackedDefault);
+        verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
+        reset(mMockNetd);
+
+        // Add ipv4 address, expect that clatd and prefix discovery are stopped and stacked
+        // linkproperties are cleaned up.
+        cellLp.addLinkAddress(myIpv4);
+        cellLp.addRoute(ipv4Subnet);
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
+        assertRoutesAdded(cellNetId, ipv4Subnet);
+        verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
+        verify(mMockDnsResolver, times(1)).stopPrefix64Discovery(cellNetId);
+
+        // As soon as stop is called, the linkproperties lose the stacked interface.
+        networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
+        LinkProperties actualLpAfterIpv4 = mCm.getLinkProperties(mCellNetworkAgent.getNetwork());
+        LinkProperties expected = new LinkProperties(cellLp);
+        expected.setNat64Prefix(kOtherNat64Prefix);
+        assertEquals(expected, actualLpAfterIpv4);
+        assertEquals(0, actualLpAfterIpv4.getStackedLinks().size());
+        assertRoutesRemoved(cellNetId, stackedDefault);
+
+        // The interface removed callback happens but has no effect after stop is called.
+        clat.interfaceRemoved(CLAT_MOBILE_IFNAME);
+        networkCallback.assertNoCallback();
+        verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
+        verifyNoMoreInteractions(mMockNetd);
+        verifyNoMoreInteractions(mMockDnsResolver);
+        reset(mMockNetd);
+        reset(mMockDnsResolver);
+        when(mMockNetd.interfaceGetCfg(CLAT_MOBILE_IFNAME))
+                .thenReturn(getClatInterfaceConfigParcel(myIpv4));
+
+        // Stopping prefix discovery causes netd to tell us that the NAT64 prefix is gone.
+        mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent(
+                cellNetId, PREFIX_OPERATION_REMOVED, kOtherNat64PrefixString, 96));
+        networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
+                (lp) -> lp.getNat64Prefix() == null);
+
+        // Remove IPv4 address and expect prefix discovery and clatd to be started again.
+        cellLp.removeLinkAddress(myIpv4);
+        cellLp.removeRoute(new RouteInfo(myIpv4, null, MOBILE_IFNAME));
+        cellLp.removeDnsServer(InetAddress.getByName("8.8.8.8"));
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
+        assertRoutesRemoved(cellNetId, ipv4Subnet);  // Directly-connected routes auto-added.
+        verify(mMockDnsResolver, times(1)).startPrefix64Discovery(cellNetId);
+        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());
+
+        // Clat iface comes up. Expect stacked link to be added.
+        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
+        networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
+                (lp) -> lp.getStackedLinks().size() == 1 && lp.getNat64Prefix() != null);
+        assertRoutesAdded(cellNetId, stackedDefault);
+        verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
+
+        // NAT64 prefix is removed. Expect that clat is stopped.
+        mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent(
+                cellNetId, PREFIX_OPERATION_REMOVED, kNat64PrefixString, 96));
+        networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
+                (lp) -> lp.getStackedLinks().size() == 0 && lp.getNat64Prefix() == null);
+        assertRoutesRemoved(cellNetId, ipv4Subnet, stackedDefault);
+
+        // Stop has no effect because clat is already stopped.
+        verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
+        networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
+                (lp) -> lp.getStackedLinks().size() == 0);
+        verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
+        verify(mMockNetd, times(1)).interfaceGetCfg(CLAT_MOBILE_IFNAME);
+        // Clean up.
+        mCellNetworkAgent.disconnect();
+        networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        networkCallback.assertNoCallback();
+        verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_CELLULAR)));
+        verify(mMockNetd).networkDestroy(cellNetId);
+        verifyNoMoreInteractions(mMockNetd);
+        reset(mMockNetd);
+
+        // Test disconnecting a network that is running 464xlat.
+
+        // Connect a network with a NAT64 prefix.
+        when(mMockNetd.interfaceGetCfg(CLAT_MOBILE_IFNAME))
+                .thenReturn(getClatInterfaceConfigParcel(myIpv4));
+        cellLp.setNat64Prefix(kNat64Prefix);
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+        mCellNetworkAgent.connect(false /* validated */);
+        networkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        cellNetId = mCellNetworkAgent.getNetwork().netId;
+        verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(cellNetId,
+                INetd.PERMISSION_NONE));
+        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());
+        clat = getNat464Xlat(mCellNetworkAgent);
+        clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true /* up */);
+        networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
+                (lp) -> lp.getStackedLinks().size() == 1
+                        && lp.getNat64Prefix().equals(kNat64Prefix));
+        verify(mMockNetd).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
+        // assertRoutesAdded sees all calls since last mMockNetd reset, so expect IPv6 routes again.
+        assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default, stackedDefault);
+        reset(mMockNetd);
+
+        // 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);
+        verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_CELLULAR)));
+        verify(mMockNetd).networkDestroy(cellNetId);
+        verifyNoMoreInteractions(mMockNetd);
+
+        mCm.unregisterNetworkCallback(networkCallback);
+    }
+
+    private void expectNat64PrefixChange(TestableNetworkCallback callback,
+            TestNetworkAgentWrapper agent, IpPrefix prefix) {
+        callback.expectLinkPropertiesThat(agent, x -> Objects.equals(x.getNat64Prefix(), prefix));
+    }
+
+    @Test
+    public void testNat64PrefixMultipleSources() throws Exception {
+        final String iface = "wlan0";
+        final String pref64FromRaStr = "64:ff9b::";
+        final String pref64FromDnsStr = "2001:db8:64::";
+        final IpPrefix pref64FromRa = new IpPrefix(InetAddress.getByName(pref64FromRaStr), 96);
+        final IpPrefix pref64FromDns = new IpPrefix(InetAddress.getByName(pref64FromDnsStr), 96);
+        final IpPrefix newPref64FromRa = new IpPrefix("2001:db8:64:64:64:64::/96");
+
+        final NetworkRequest request = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+
+        final LinkProperties baseLp = new LinkProperties();
+        baseLp.setInterfaceName(iface);
+        baseLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64"));
+        baseLp.addDnsServer(InetAddress.getByName("2001:4860:4860::6464"));
+
+        reset(mMockNetd, mMockDnsResolver);
+        InOrder inOrder = inOrder(mMockNetd, mMockDnsResolver);
+
+        // If a network already has a NAT64 prefix on connect, clatd is started immediately and
+        // prefix discovery is never started.
+        LinkProperties lp = new LinkProperties(baseLp);
+        lp.setNat64Prefix(pref64FromRa);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp);
+        mWiFiNetworkAgent.connect(false);
+        final Network network = mWiFiNetworkAgent.getNetwork();
+        int netId = network.getNetId();
+        callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+        inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString());
+        inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+        callback.assertNoCallback();
+        assertEquals(pref64FromRa, mCm.getLinkProperties(network).getNat64Prefix());
+
+        // If the RA prefix is withdrawn, clatd is stopped and prefix discovery is started.
+        lp.setNat64Prefix(null);
+        mWiFiNetworkAgent.sendLinkProperties(lp);
+        expectNat64PrefixChange(callback, mWiFiNetworkAgent, null);
+        inOrder.verify(mMockNetd).clatdStop(iface);
+        inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
+        inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
+
+        // If the RA prefix appears while DNS discovery is in progress, discovery is stopped and
+        // clatd is started with the prefix from the RA.
+        lp.setNat64Prefix(pref64FromRa);
+        mWiFiNetworkAgent.sendLinkProperties(lp);
+        expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromRa);
+        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+        inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString());
+
+        // Withdraw the RA prefix so we can test the case where an RA prefix appears after DNS
+        // discovery has succeeded.
+        lp.setNat64Prefix(null);
+        mWiFiNetworkAgent.sendLinkProperties(lp);
+        expectNat64PrefixChange(callback, mWiFiNetworkAgent, null);
+        inOrder.verify(mMockNetd).clatdStop(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());
+
+        // 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());
+        inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
+
+        // If the RA is later withdrawn, nothing happens again.
+        lp.setNat64Prefix(null);
+        mWiFiNetworkAgent.sendLinkProperties(lp);
+        callback.assertNoCallback();
+        inOrder.verify(mMockNetd, never()).clatdStop(iface);
+        inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+        inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
+
+        // If the RA prefix changes, clatd is restarted and prefix discovery is stopped.
+        lp.setNat64Prefix(pref64FromRa);
+        mWiFiNetworkAgent.sendLinkProperties(lp);
+        expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromRa);
+        inOrder.verify(mMockNetd).clatdStop(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());
+        inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString());
+        inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+
+        // If the RA prefix changes, clatd is restarted and prefix discovery is not started.
+        lp.setNat64Prefix(newPref64FromRa);
+        mWiFiNetworkAgent.sendLinkProperties(lp);
+        expectNat64PrefixChange(callback, mWiFiNetworkAgent, newPref64FromRa);
+        inOrder.verify(mMockNetd).clatdStop(iface);
+        inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
+        inOrder.verify(mMockNetd).clatdStart(iface, newPref64FromRa.toString());
+        inOrder.verify(mMockDnsResolver).setPrefix64(netId, newPref64FromRa.toString());
+        inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+
+        // If the RA prefix changes to the same value, nothing happens.
+        lp.setNat64Prefix(newPref64FromRa);
+        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());
+        inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
+
+        // The transition between no prefix and DNS prefix is tested in testStackedLinkProperties.
+
+        // If the same prefix is learned first by DNS and then by RA, and clat is later stopped,
+        // (e.g., because the network disconnects) setPrefix64(netid, "") is never called.
+        lp.setNat64Prefix(null);
+        mWiFiNetworkAgent.sendLinkProperties(lp);
+        expectNat64PrefixChange(callback, mWiFiNetworkAgent, null);
+        inOrder.verify(mMockNetd).clatdStop(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());
+        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());
+        inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
+
+        // When tearing down a network, clat state is only updated after CALLBACK_LOST is fired, but
+        // before CONNECTIVITY_ACTION is sent. Wait for CONNECTIVITY_ACTION before verifying that
+        // clat has been stopped, or the test will be flaky.
+        ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED);
+        mWiFiNetworkAgent.disconnect();
+        callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        b.expectBroadcast();
+
+        inOrder.verify(mMockNetd).clatdStop(iface);
+        inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
+
+        mCm.unregisterNetworkCallback(callback);
+    }
+
+    @Test
+    public void testWith464XlatDisable() throws Exception {
+        doReturn(false).when(mDeps).getCellular464XlatEnabled();
+
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        final TestNetworkCallback defaultCallback = new TestNetworkCallback();
+        final NetworkRequest networkRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        mCm.registerNetworkCallback(networkRequest, callback);
+        mCm.registerDefaultNetworkCallback(defaultCallback);
+
+        // Bring up validated cell.
+        final LinkProperties cellLp = new LinkProperties();
+        cellLp.setInterfaceName(MOBILE_IFNAME);
+        cellLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64"));
+        cellLp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, MOBILE_IFNAME));
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        mCellNetworkAgent.connect(true);
+        callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        final int cellNetId = mCellNetworkAgent.getNetwork().netId;
+        waitForIdle();
+
+        verify(mMockDnsResolver, never()).startPrefix64Discovery(cellNetId);
+        Nat464Xlat clat = getNat464Xlat(mCellNetworkAgent);
+        assertTrue("Nat464Xlat was not IDLE", !clat.isStarted());
+
+        // This cannot happen because prefix discovery cannot succeed if it is never started.
+        mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
+                makeNat64PrefixEvent(cellNetId, PREFIX_OPERATION_ADDED, "64:ff9b::", 96));
+
+        // ... but still, check that even if it did, clatd would not be started.
+        verify(mMockNetd, never()).clatdStart(anyString(), anyString());
+        assertTrue("Nat464Xlat was not IDLE", !clat.isStarted());
+    }
+
+    @Test
+    public void testDataActivityTracking() throws Exception {
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        final NetworkRequest networkRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        mCm.registerNetworkCallback(networkRequest, networkCallback);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        final LinkProperties cellLp = new LinkProperties();
+        cellLp.setInterfaceName(MOBILE_IFNAME);
+        mCellNetworkAgent.sendLinkProperties(cellLp);
+        mCellNetworkAgent.connect(true);
+        networkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_CELLULAR)));
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        final LinkProperties wifiLp = new LinkProperties();
+        wifiLp.setInterfaceName(WIFI_IFNAME);
+        mWiFiNetworkAgent.sendLinkProperties(wifiLp);
+
+        // Network switch
+        mWiFiNetworkAgent.connect(true);
+        networkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        networkCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        networkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_WIFI)));
+        verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_CELLULAR)));
+
+        // Disconnect wifi and switch back to cell
+        reset(mMockNetd);
+        mWiFiNetworkAgent.disconnect();
+        networkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        assertNoCallbacks(networkCallback);
+        verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_WIFI)));
+        verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_CELLULAR)));
+
+        // reconnect wifi
+        reset(mMockNetd);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        wifiLp.setInterfaceName(WIFI_IFNAME);
+        mWiFiNetworkAgent.sendLinkProperties(wifiLp);
+        mWiFiNetworkAgent.connect(true);
+        networkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        networkCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        networkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_WIFI)));
+        verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_CELLULAR)));
+
+        // Disconnect cell
+        reset(mMockNetd);
+        mCellNetworkAgent.disconnect();
+        networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        // LOST callback is triggered earlier than removing idle timer. Broadcast should also be
+        // sent as network being switched. Ensure rule removal for cell will not be triggered
+        // unexpectedly before network being removed.
+        waitForIdle();
+        verify(mMockNetd, times(0)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_CELLULAR)));
+        verify(mMockNetd, times(1)).networkDestroy(eq(mCellNetworkAgent.getNetwork().netId));
+        verify(mMockDnsResolver, times(1))
+                .destroyNetworkCache(eq(mCellNetworkAgent.getNetwork().netId));
+
+        // Disconnect wifi
+        ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED);
+        mWiFiNetworkAgent.disconnect();
+        b.expectBroadcast();
+        verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(),
+                eq(Integer.toString(TRANSPORT_WIFI)));
+
+        // Clean up
+        mCm.unregisterNetworkCallback(networkCallback);
+    }
+
+    private void verifyTcpBufferSizeChange(String tcpBufferSizes) throws Exception {
+        String[] values = tcpBufferSizes.split(",");
+        String rmemValues = String.join(" ", values[0], values[1], values[2]);
+        String wmemValues = String.join(" ", values[3], values[4], values[5]);
+        verify(mMockNetd, atLeastOnce()).setTcpRWmemorySize(rmemValues, wmemValues);
+        reset(mMockNetd);
+    }
+
+    @Test
+    public void testTcpBufferReset() throws Exception {
+        final String testTcpBufferSizes = "1,2,3,4,5,6";
+        final NetworkRequest networkRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(networkRequest, networkCallback);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        reset(mMockNetd);
+        // Switching default network updates TCP buffer sizes.
+        mCellNetworkAgent.connect(false);
+        networkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        verifyTcpBufferSizeChange(ConnectivityService.DEFAULT_TCP_BUFFER_SIZES);
+        // Change link Properties should have updated tcp buffer size.
+        LinkProperties lp = new LinkProperties();
+        lp.setTcpBufferSizes(testTcpBufferSizes);
+        mCellNetworkAgent.sendLinkProperties(lp);
+        networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
+        verifyTcpBufferSizeChange(testTcpBufferSizes);
+        // Clean up.
+        mCellNetworkAgent.disconnect();
+        networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        networkCallback.assertNoCallback();
+        mCm.unregisterNetworkCallback(networkCallback);
+    }
+
+    @Test
+    public void testGetGlobalProxyForNetwork() throws Exception {
+        final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        final Network wifiNetwork = mWiFiNetworkAgent.getNetwork();
+        when(mService.mProxyTracker.getGlobalProxy()).thenReturn(testProxyInfo);
+        assertEquals(testProxyInfo, mService.getProxyForNetwork(wifiNetwork));
+    }
+
+    @Test
+    public void testGetProxyForActiveNetwork() throws Exception {
+        final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888);
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        waitForIdle();
+        assertNull(mService.getProxyForNetwork(null));
+
+        final LinkProperties testLinkProperties = new LinkProperties();
+        testLinkProperties.setHttpProxy(testProxyInfo);
+
+        mWiFiNetworkAgent.sendLinkProperties(testLinkProperties);
+        waitForIdle();
+
+        assertEquals(testProxyInfo, mService.getProxyForNetwork(null));
+    }
+
+    @Test
+    public void testGetProxyForVPN() throws Exception {
+        final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888);
+
+        // Set up a WiFi network with no proxy
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        waitForIdle();
+        assertNull(mService.getProxyForNetwork(null));
+
+        // Connect a VPN network with a proxy.
+        LinkProperties testLinkProperties = new LinkProperties();
+        testLinkProperties.setHttpProxy(testProxyInfo);
+        mMockVpn.establishForMyUid(testLinkProperties);
+        assertUidRangesUpdatedForMyUid(true);
+
+        // Test that the VPN network returns a proxy, and the WiFi does not.
+        assertEquals(testProxyInfo, mService.getProxyForNetwork(mMockVpn.getNetwork()));
+        assertEquals(testProxyInfo, mService.getProxyForNetwork(null));
+        assertNull(mService.getProxyForNetwork(mWiFiNetworkAgent.getNetwork()));
+
+        // Test that the VPN network returns no proxy when it is set to null.
+        testLinkProperties.setHttpProxy(null);
+        mMockVpn.sendLinkProperties(testLinkProperties);
+        waitForIdle();
+        assertNull(mService.getProxyForNetwork(mMockVpn.getNetwork()));
+        assertNull(mService.getProxyForNetwork(null));
+
+        // Set WiFi proxy and check that the vpn proxy is still null.
+        testLinkProperties.setHttpProxy(testProxyInfo);
+        mWiFiNetworkAgent.sendLinkProperties(testLinkProperties);
+        waitForIdle();
+        assertNull(mService.getProxyForNetwork(null));
+
+        // Disconnect from VPN and check that the active network, which is now the WiFi, has the
+        // correct proxy setting.
+        mMockVpn.disconnect();
+        waitForIdle();
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertEquals(testProxyInfo, mService.getProxyForNetwork(mWiFiNetworkAgent.getNetwork()));
+        assertEquals(testProxyInfo, mService.getProxyForNetwork(null));
+    }
+
+    @Test
+    public void testFullyRoutedVpnResultsInInterfaceFilteringRules() throws Exception {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE));
+        // The uid range needs to cover the test app so the network is visible to it.
+        final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
+        mMockVpn.establish(lp, VPN_UID, vpnRange);
+        assertVpnUidRangesUpdated(true, vpnRange, VPN_UID);
+
+        // A connected VPN should have interface rules set up. 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(mMockNetd, times(2)).firewallAddUidInterfaceRules(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));
+
+        mMockVpn.disconnect();
+        waitForIdle();
+
+        // Disconnected VPN should have interface rules removed
+        verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture());
+        assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
+        assertNull(mService.mPermissionMonitor.getVpnUidRanges("tun0"));
+    }
+
+    @Test
+    public void testLegacyVpnDoesNotResultInInterfaceFilteringRule() throws Exception {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
+        // The uid range needs to cover the test app so the network is visible to it.
+        final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
+        mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
+        assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
+
+        // Legacy VPN should not have interface rules set up
+        verify(mMockNetd, never()).firewallAddUidInterfaceRules(any(), any());
+    }
+
+    @Test
+    public void testLocalIpv4OnlyVpnDoesNotResultInInterfaceFilteringRule()
+            throws Exception {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0"));
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE));
+        // The uid range needs to cover the test app so the network is visible to it.
+        final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
+        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(mMockNetd, never()).firewallAddUidInterfaceRules(any(), any());
+    }
+
+    @Test
+    public void testVpnHandoverChangesInterfaceFilteringRule() throws Exception {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
+        // The uid range needs to cover the test app so the network is visible to it.
+        final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
+        mMockVpn.establish(lp, VPN_UID, vpnRange);
+        assertVpnUidRangesUpdated(true, vpnRange, VPN_UID);
+
+        // Connected VPN should have interface rules set up. There are two expected invocations,
+        // one during VPN uid update, one during VPN LinkProperties update
+        ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
+        verify(mMockNetd, times(2)).firewallAddUidInterfaceRules(eq("tun0"), uidCaptor.capture());
+        assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID);
+        assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID);
+
+        reset(mMockNetd);
+        InOrder inOrder = inOrder(mMockNetd);
+        lp.setInterfaceName("tun1");
+        mMockVpn.sendLinkProperties(lp);
+        waitForIdle();
+        // VPN handover (switch to a new interface) should result in rules being updated (old rules
+        // removed first, then new rules added)
+        inOrder.verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture());
+        assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
+        inOrder.verify(mMockNetd).firewallAddUidInterfaceRules(eq("tun1"), uidCaptor.capture());
+        assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
+
+        reset(mMockNetd);
+        lp = new LinkProperties();
+        lp.setInterfaceName("tun1");
+        lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun1"));
+        mMockVpn.sendLinkProperties(lp);
+        waitForIdle();
+        // VPN not routing everything should no longer have interface filtering rules
+        verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture());
+        assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
+
+        reset(mMockNetd);
+        lp = new LinkProperties();
+        lp.setInterfaceName("tun1");
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE));
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
+        mMockVpn.sendLinkProperties(lp);
+        waitForIdle();
+        // Back to routing all IPv6 traffic should have filtering rules
+        verify(mMockNetd).firewallAddUidInterfaceRules(eq("tun1"), uidCaptor.capture());
+        assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
+    }
+
+    @Test
+    public void testUidUpdateChangesInterfaceFilteringRule() throws Exception {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE));
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
+        // The uid range needs to cover the test app so the network is visible to it.
+        final UidRange vpnRange = PRIMARY_UIDRANGE;
+        final Set<UidRange> vpnRanges = Collections.singleton(vpnRange);
+        mMockVpn.establish(lp, VPN_UID, vpnRanges);
+        assertVpnUidRangesUpdated(true, vpnRanges, VPN_UID);
+
+        reset(mMockNetd);
+        InOrder inOrder = inOrder(mMockNetd);
+
+        // Update to new range which is old range minus APP1, i.e. only APP2
+        final Set<UidRange> newRanges = new HashSet<>(Arrays.asList(
+                new UidRange(vpnRange.start, APP1_UID - 1),
+                new UidRange(APP1_UID + 1, vpnRange.stop)));
+        mMockVpn.setUids(newRanges);
+        waitForIdle();
+
+        ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
+        // Verify old rules are removed before new rules are added
+        inOrder.verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture());
+        assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
+        inOrder.verify(mMockNetd).firewallAddUidInterfaceRules(eq("tun0"), uidCaptor.capture());
+        assertContainsExactly(uidCaptor.getValue(), APP2_UID);
+    }
+
+    @Test
+    public void testLinkPropertiesWithWakeOnLanForActiveNetwork() throws Exception {
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+
+        LinkProperties wifiLp = new LinkProperties();
+        wifiLp.setInterfaceName(WIFI_WOL_IFNAME);
+        wifiLp.setWakeOnLanSupported(false);
+
+        // Default network switch should update ifaces.
+        mWiFiNetworkAgent.connect(false);
+        mWiFiNetworkAgent.sendLinkProperties(wifiLp);
+        waitForIdle();
+
+        // ConnectivityService should have changed the WakeOnLanSupported to true
+        wifiLp.setWakeOnLanSupported(true);
+        assertEquals(wifiLp, mService.getActiveLinkProperties());
+    }
+
+    @Test
+    public void testLegacyExtraInfoSentToNetworkMonitor() throws Exception {
+        class TestNetworkAgent extends NetworkAgent {
+            TestNetworkAgent(Context context, Looper looper, NetworkAgentConfig config) {
+                super(context, looper, "MockAgent", new NetworkCapabilities(),
+                        new LinkProperties(), 40 , config, null /* provider */);
+            }
+        }
+        final NetworkAgent naNoExtraInfo = new TestNetworkAgent(
+                mServiceContext, mCsHandlerThread.getLooper(), new NetworkAgentConfig());
+        naNoExtraInfo.register();
+        verify(mNetworkStack).makeNetworkMonitor(any(), isNull(String.class), any());
+        naNoExtraInfo.unregister();
+
+        reset(mNetworkStack);
+        final NetworkAgentConfig config =
+                new NetworkAgentConfig.Builder().setLegacyExtraInfo("legacyinfo").build();
+        final NetworkAgent naExtraInfo = new TestNetworkAgent(
+                mServiceContext, mCsHandlerThread.getLooper(), config);
+        naExtraInfo.register();
+        verify(mNetworkStack).makeNetworkMonitor(any(), eq("legacyinfo"), any());
+        naExtraInfo.unregister();
+    }
+
+    // To avoid granting location permission bypass.
+    private void denyAllLocationPrivilegedPermissions() {
+        mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+                PERMISSION_DENIED);
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED);
+        mServiceContext.setPermission(NETWORK_STACK, PERMISSION_DENIED);
+        mServiceContext.setPermission(Manifest.permission.NETWORK_SETUP_WIZARD,
+                PERMISSION_DENIED);
+    }
+
+    private void setupLocationPermissions(
+            int targetSdk, boolean locationToggle, String op, String perm) throws Exception {
+        denyAllLocationPrivilegedPermissions();
+
+        final ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.targetSdkVersion = targetSdk;
+        when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), any()))
+                .thenReturn(applicationInfo);
+        when(mPackageManager.getTargetSdkVersion(any())).thenReturn(targetSdk);
+
+        when(mLocationManager.isLocationEnabledForUser(any())).thenReturn(locationToggle);
+
+        if (op != null) {
+            when(mAppOpsManager.noteOp(eq(op), eq(Process.myUid()),
+                    eq(mContext.getPackageName()), eq(getAttributionTag()), anyString()))
+                .thenReturn(AppOpsManager.MODE_ALLOWED);
+        }
+
+        if (perm != null) {
+            mServiceContext.setPermission(perm, PERMISSION_GRANTED);
+        }
+    }
+
+    private int getOwnerUidNetCapsPermission(int ownerUid, int callerUid,
+            boolean includeLocationSensitiveInfo) {
+        final NetworkCapabilities netCap = new NetworkCapabilities().setOwnerUid(ownerUid);
+
+        return mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                netCap, includeLocationSensitiveInfo, Process.myUid(), callerUid,
+                mContext.getPackageName(), getAttributionTag())
+                .getOwnerUid();
+    }
+
+    private void verifyTransportInfoCopyNetCapsPermission(
+            int callerUid, boolean includeLocationSensitiveInfo,
+            boolean shouldMakeCopyWithLocationSensitiveFieldsParcelable) {
+        final TransportInfo transportInfo = mock(TransportInfo.class);
+        when(transportInfo.getApplicableRedactions()).thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION);
+        final NetworkCapabilities netCap =
+                new NetworkCapabilities().setTransportInfo(transportInfo);
+
+        mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                netCap, includeLocationSensitiveInfo, Process.myPid(), callerUid,
+                mContext.getPackageName(), getAttributionTag());
+        if (shouldMakeCopyWithLocationSensitiveFieldsParcelable) {
+            verify(transportInfo).makeCopy(REDACT_NONE);
+        } else {
+            verify(transportInfo).makeCopy(REDACT_FOR_ACCESS_FINE_LOCATION);
+        }
+    }
+
+    private void verifyOwnerUidAndTransportInfoNetCapsPermission(
+            boolean shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag,
+            boolean shouldInclLocationSensitiveOwnerUidWithIncludeFlag,
+            boolean shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag,
+            boolean shouldInclLocationSensitiveTransportInfoWithIncludeFlag) {
+        final int myUid = Process.myUid();
+
+        final int expectedOwnerUidWithoutIncludeFlag =
+                shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag
+                        ? myUid : INVALID_UID;
+        assertEquals(expectedOwnerUidWithoutIncludeFlag, getOwnerUidNetCapsPermission(
+                myUid, myUid, false /* includeLocationSensitiveInfo */));
+
+        final int expectedOwnerUidWithIncludeFlag =
+                shouldInclLocationSensitiveOwnerUidWithIncludeFlag ? myUid : INVALID_UID;
+        assertEquals(expectedOwnerUidWithIncludeFlag, getOwnerUidNetCapsPermission(
+                myUid, myUid, true /* includeLocationSensitiveInfo */));
+
+        verifyTransportInfoCopyNetCapsPermission(myUid,
+                false, /* includeLocationSensitiveInfo */
+                shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag);
+
+        verifyTransportInfoCopyNetCapsPermission(myUid,
+                true, /* includeLocationSensitiveInfo */
+                shouldInclLocationSensitiveTransportInfoWithIncludeFlag);
+
+    }
+
+    private void verifyOwnerUidAndTransportInfoNetCapsPermissionPreS() {
+        verifyOwnerUidAndTransportInfoNetCapsPermission(
+                // Ensure that owner uid is included even if the request asks to remove it (which is
+                // the default) since the app has necessary permissions and targetSdk < S.
+                true, /* shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag */
+                true, /* shouldInclLocationSensitiveOwnerUidWithIncludeFlag */
+                // Ensure that location info is removed if the request asks to remove it even if the
+                // app has necessary permissions.
+                false, /* shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag */
+                true /* shouldInclLocationSensitiveTransportInfoWithIncludeFlag */
+        );
+    }
+
+    @Test
+    public void testCreateWithLocationInfoSanitizedWithFineLocationAfterQPreS()
+            throws Exception {
+        setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION,
+                Manifest.permission.ACCESS_FINE_LOCATION);
+
+        verifyOwnerUidAndTransportInfoNetCapsPermissionPreS();
+    }
+
+    @Test
+    public void testCreateWithLocationInfoSanitizedWithFineLocationPreSWithAndWithoutCallbackFlag()
+            throws Exception {
+        setupLocationPermissions(Build.VERSION_CODES.R, true, AppOpsManager.OPSTR_FINE_LOCATION,
+                Manifest.permission.ACCESS_FINE_LOCATION);
+
+        verifyOwnerUidAndTransportInfoNetCapsPermissionPreS();
+    }
+
+    @Test
+    public void
+            testCreateWithLocationInfoSanitizedWithFineLocationAfterSWithAndWithoutCallbackFlag()
+            throws Exception {
+        setupLocationPermissions(Build.VERSION_CODES.S, true, AppOpsManager.OPSTR_FINE_LOCATION,
+                Manifest.permission.ACCESS_FINE_LOCATION);
+
+        verifyOwnerUidAndTransportInfoNetCapsPermission(
+                // Ensure that the owner UID is removed if the request asks us to remove it even
+                // if the app has necessary permissions since targetSdk >= S.
+                false, /* shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag */
+                true, /* shouldInclLocationSensitiveOwnerUidWithIncludeFlag */
+                // Ensure that location info is removed if the request asks to remove it even if the
+                // app has necessary permissions.
+                false, /* shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag */
+                true /* shouldInclLocationSensitiveTransportInfoWithIncludeFlag */
+        );
+    }
+
+    @Test
+    public void testCreateWithLocationInfoSanitizedWithCoarseLocationPreQ()
+            throws Exception {
+        setupLocationPermissions(Build.VERSION_CODES.P, true, AppOpsManager.OPSTR_COARSE_LOCATION,
+                Manifest.permission.ACCESS_COARSE_LOCATION);
+
+        verifyOwnerUidAndTransportInfoNetCapsPermissionPreS();
+    }
+
+    private void verifyOwnerUidAndTransportInfoNetCapsNotIncluded() {
+        verifyOwnerUidAndTransportInfoNetCapsPermission(
+                false, /* shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag */
+                false, /* shouldInclLocationSensitiveOwnerUidWithIncludeFlag */
+                false, /* shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag */
+                false /* shouldInclLocationSensitiveTransportInfoWithIncludeFlag */
+        );
+    }
+
+    @Test
+    public void testCreateWithLocationInfoSanitizedLocationOff() throws Exception {
+        // Test that even with fine location permission, and UIDs matching, the UID is sanitized.
+        setupLocationPermissions(Build.VERSION_CODES.Q, false, AppOpsManager.OPSTR_FINE_LOCATION,
+                Manifest.permission.ACCESS_FINE_LOCATION);
+
+        verifyOwnerUidAndTransportInfoNetCapsNotIncluded();
+    }
+
+    @Test
+    public void testCreateWithLocationInfoSanitizedWrongUid() throws Exception {
+        // Test that even with fine location permission, not being the owner leads to sanitization.
+        setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION,
+                Manifest.permission.ACCESS_FINE_LOCATION);
+
+        final int myUid = Process.myUid();
+        assertEquals(Process.INVALID_UID,
+                getOwnerUidNetCapsPermission(myUid + 1, myUid,
+                        true /* includeLocationSensitiveInfo */));
+    }
+
+    @Test
+    public void testCreateWithLocationInfoSanitizedWithCoarseLocationAfterQ()
+            throws Exception {
+        // Test that not having fine location permission leads to sanitization.
+        setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_COARSE_LOCATION,
+                Manifest.permission.ACCESS_COARSE_LOCATION);
+
+        verifyOwnerUidAndTransportInfoNetCapsNotIncluded();
+    }
+
+    @Test
+    public void testCreateWithLocationInfoSanitizedWithCoarseLocationAfterS()
+            throws Exception {
+        // Test that not having fine location permission leads to sanitization.
+        setupLocationPermissions(Build.VERSION_CODES.S, true, AppOpsManager.OPSTR_COARSE_LOCATION,
+                Manifest.permission.ACCESS_COARSE_LOCATION);
+
+        verifyOwnerUidAndTransportInfoNetCapsNotIncluded();
+    }
+
+    @Test
+    public void testCreateForCallerWithLocalMacAddressSanitizedWithLocalMacAddressPermission()
+            throws Exception {
+        mServiceContext.setPermission(Manifest.permission.LOCAL_MAC_ADDRESS, PERMISSION_GRANTED);
+
+        final TransportInfo transportInfo = mock(TransportInfo.class);
+        when(transportInfo.getApplicableRedactions())
+                .thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS);
+        final NetworkCapabilities netCap =
+                new NetworkCapabilities().setTransportInfo(transportInfo);
+
+        mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                netCap, false /* includeLocationSensitiveInfoInTransportInfo */,
+                Process.myPid(), Process.myUid(),
+                mContext.getPackageName(), getAttributionTag());
+        // don't redact MAC_ADDRESS fields, only location sensitive fields.
+        verify(transportInfo).makeCopy(REDACT_FOR_ACCESS_FINE_LOCATION);
+    }
+
+    @Test
+    public void testCreateForCallerWithLocalMacAddressSanitizedWithoutLocalMacAddressPermission()
+            throws Exception {
+        mServiceContext.setPermission(Manifest.permission.LOCAL_MAC_ADDRESS, PERMISSION_DENIED);
+
+        final TransportInfo transportInfo = mock(TransportInfo.class);
+        when(transportInfo.getApplicableRedactions())
+                .thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS);
+        final NetworkCapabilities netCap =
+                new NetworkCapabilities().setTransportInfo(transportInfo);
+
+        mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                netCap, false /* includeLocationSensitiveInfoInTransportInfo */,
+                Process.myPid(), Process.myUid(),
+                mContext.getPackageName(), getAttributionTag());
+        // redact both MAC_ADDRESS & location sensitive fields.
+        verify(transportInfo).makeCopy(REDACT_FOR_ACCESS_FINE_LOCATION
+                | REDACT_FOR_LOCAL_MAC_ADDRESS);
+    }
+
+    @Test
+    public void testCreateForCallerWithLocalMacAddressSanitizedWithSettingsPermission()
+            throws Exception {
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+        final TransportInfo transportInfo = mock(TransportInfo.class);
+        when(transportInfo.getApplicableRedactions())
+                .thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_NETWORK_SETTINGS);
+        final NetworkCapabilities netCap =
+                new NetworkCapabilities().setTransportInfo(transportInfo);
+
+        mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                netCap, false /* includeLocationSensitiveInfoInTransportInfo */,
+                Process.myPid(), Process.myUid(),
+                mContext.getPackageName(), getAttributionTag());
+        // don't redact NETWORK_SETTINGS fields, only location sensitive fields.
+        verify(transportInfo).makeCopy(REDACT_FOR_ACCESS_FINE_LOCATION);
+    }
+
+    @Test
+    public void testCreateForCallerWithLocalMacAddressSanitizedWithoutSettingsPermission()
+            throws Exception {
+        mServiceContext.setPermission(Manifest.permission.LOCAL_MAC_ADDRESS, PERMISSION_DENIED);
+
+        final TransportInfo transportInfo = mock(TransportInfo.class);
+        when(transportInfo.getApplicableRedactions())
+                .thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_NETWORK_SETTINGS);
+        final NetworkCapabilities netCap =
+                new NetworkCapabilities().setTransportInfo(transportInfo);
+
+        mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                netCap, false /* includeLocationSensitiveInfoInTransportInfo */,
+                Process.myPid(), Process.myUid(),
+                mContext.getPackageName(), getAttributionTag());
+        // redact both NETWORK_SETTINGS & location sensitive fields.
+        verify(transportInfo).makeCopy(
+                REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_NETWORK_SETTINGS);
+    }
+
+    /**
+     * Test TransportInfo to verify redaction mechanism.
+     */
+    private static class TestTransportInfo implements TransportInfo {
+        public final boolean locationRedacted;
+        public final boolean localMacAddressRedacted;
+        public final boolean settingsRedacted;
+
+        TestTransportInfo() {
+            locationRedacted = false;
+            localMacAddressRedacted = false;
+            settingsRedacted = false;
+        }
+
+        TestTransportInfo(boolean locationRedacted, boolean localMacAddressRedacted,
+                boolean settingsRedacted) {
+            this.locationRedacted = locationRedacted;
+            this.localMacAddressRedacted =
+                    localMacAddressRedacted;
+            this.settingsRedacted = settingsRedacted;
+        }
+
+        @Override
+        public TransportInfo makeCopy(@NetworkCapabilities.RedactionType long redactions) {
+            return new TestTransportInfo(
+                    locationRedacted | (redactions & REDACT_FOR_ACCESS_FINE_LOCATION) != 0,
+                    localMacAddressRedacted | (redactions & REDACT_FOR_LOCAL_MAC_ADDRESS) != 0,
+                    settingsRedacted | (redactions & REDACT_FOR_NETWORK_SETTINGS) != 0
+            );
+        }
+
+        @Override
+        public @NetworkCapabilities.RedactionType long getApplicableRedactions() {
+            return REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS
+                    | REDACT_FOR_NETWORK_SETTINGS;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (!(other instanceof TestTransportInfo)) return false;
+            TestTransportInfo that = (TestTransportInfo) other;
+            return that.locationRedacted == this.locationRedacted
+                    && that.localMacAddressRedacted == this.localMacAddressRedacted
+                    && that.settingsRedacted == this.settingsRedacted;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(locationRedacted, localMacAddressRedacted, settingsRedacted);
+        }
+
+        @Override
+        public String toString() {
+            return String.format(
+                    "TestTransportInfo{locationRedacted=%s macRedacted=%s settingsRedacted=%s}",
+                    locationRedacted, localMacAddressRedacted, settingsRedacted);
+        }
+    }
+
+    private TestTransportInfo getTestTransportInfo(NetworkCapabilities nc) {
+        return (TestTransportInfo) nc.getTransportInfo();
+    }
+
+    private TestTransportInfo getTestTransportInfo(TestNetworkAgentWrapper n) {
+        final NetworkCapabilities nc = mCm.getNetworkCapabilities(n.getNetwork());
+        assertNotNull(nc);
+        return getTestTransportInfo(nc);
+    }
+
+
+    private void verifyNetworkCallbackLocationDataInclusionUsingTransportInfoAndOwnerUidInNetCaps(
+            @NonNull TestNetworkCallback wifiNetworkCallback, int actualOwnerUid,
+            @NonNull TransportInfo actualTransportInfo, int expectedOwnerUid,
+            @NonNull TransportInfo expectedTransportInfo) throws Exception {
+        when(mPackageManager.getTargetSdkVersion(anyString())).thenReturn(Build.VERSION_CODES.S);
+        final NetworkCapabilities ncTemplate =
+                new NetworkCapabilities()
+                        .addTransportType(TRANSPORT_WIFI)
+                        .setOwnerUid(actualOwnerUid);
+
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(),
+                ncTemplate);
+        mWiFiNetworkAgent.connect(false);
+
+        wifiNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+
+        // Send network capabilities update with TransportInfo to trigger capabilities changed
+        // callback.
+        mWiFiNetworkAgent.setNetworkCapabilities(
+                ncTemplate.setTransportInfo(actualTransportInfo), true);
+
+        wifiNetworkCallback.expectCapabilitiesThat(mWiFiNetworkAgent,
+                nc -> Objects.equals(expectedOwnerUid, nc.getOwnerUid())
+                        && Objects.equals(expectedTransportInfo, nc.getTransportInfo()));
+    }
+
+    @Test
+    public void testVerifyLocationDataIsNotIncludedWhenInclFlagNotSet() throws Exception {
+        final TestNetworkCallback wifiNetworkCallack = new TestNetworkCallback();
+        final int ownerUid = Process.myUid();
+        final TransportInfo transportInfo = new TestTransportInfo();
+        // Even though the test uid holds privileged permissions, mask location fields since
+        // the callback did not explicitly opt-in to get location data.
+        final TransportInfo sanitizedTransportInfo = new TestTransportInfo(
+                true, /* locationRedacted */
+                true, /* localMacAddressRedacted */
+                true /* settingsRedacted */
+        );
+        // Should not expect location data since the callback does not set the flag for including
+        // location data.
+        verifyNetworkCallbackLocationDataInclusionUsingTransportInfoAndOwnerUidInNetCaps(
+                wifiNetworkCallack, ownerUid, transportInfo, INVALID_UID, sanitizedTransportInfo);
+    }
+
+    @Test
+    public void testTransportInfoRedactionInSynchronousCalls() throws Exception {
+        final NetworkCapabilities ncTemplate = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_WIFI)
+                .setTransportInfo(new TestTransportInfo());
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(),
+                ncTemplate);
+        mWiFiNetworkAgent.connect(true /* validated; waits for callback */);
+
+        // NETWORK_SETTINGS redaction is controlled by the NETWORK_SETTINGS permission
+        assertTrue(getTestTransportInfo(mWiFiNetworkAgent).settingsRedacted);
+        withPermission(NETWORK_SETTINGS, () -> {
+            assertFalse(getTestTransportInfo(mWiFiNetworkAgent).settingsRedacted);
+        });
+        assertTrue(getTestTransportInfo(mWiFiNetworkAgent).settingsRedacted);
+
+        // LOCAL_MAC_ADDRESS redaction is controlled by the LOCAL_MAC_ADDRESS permission
+        assertTrue(getTestTransportInfo(mWiFiNetworkAgent).localMacAddressRedacted);
+        withPermission(LOCAL_MAC_ADDRESS, () -> {
+            assertFalse(getTestTransportInfo(mWiFiNetworkAgent).localMacAddressRedacted);
+        });
+        assertTrue(getTestTransportInfo(mWiFiNetworkAgent).localMacAddressRedacted);
+
+        // Synchronous getNetworkCapabilities calls never return unredacted location-sensitive
+        // information.
+        assertTrue(getTestTransportInfo(mWiFiNetworkAgent).locationRedacted);
+        setupLocationPermissions(Build.VERSION_CODES.S, true, AppOpsManager.OPSTR_FINE_LOCATION,
+                Manifest.permission.ACCESS_FINE_LOCATION);
+        assertTrue(getTestTransportInfo(mWiFiNetworkAgent).locationRedacted);
+        denyAllLocationPrivilegedPermissions();
+        assertTrue(getTestTransportInfo(mWiFiNetworkAgent).locationRedacted);
+    }
+
+    private void setupConnectionOwnerUid(int vpnOwnerUid, @VpnManager.VpnType int vpnType)
+            throws Exception {
+        final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
+        mMockVpn.setVpnType(vpnType);
+        mMockVpn.establish(new LinkProperties(), vpnOwnerUid, vpnRange);
+        assertVpnUidRangesUpdated(true, vpnRange, vpnOwnerUid);
+
+        final UnderlyingNetworkInfo underlyingNetworkInfo =
+                new UnderlyingNetworkInfo(vpnOwnerUid, VPN_IFNAME, new ArrayList<String>());
+        mMockVpn.setUnderlyingNetworkInfo(underlyingNetworkInfo);
+        when(mDeps.getConnectionOwnerUid(anyInt(), any(), any())).thenReturn(42);
+    }
+
+    private void setupConnectionOwnerUidAsVpnApp(int vpnOwnerUid, @VpnManager.VpnType int vpnType)
+            throws Exception {
+        setupConnectionOwnerUid(vpnOwnerUid, vpnType);
+
+        // Test as VPN app
+        mServiceContext.setPermission(NETWORK_STACK, PERMISSION_DENIED);
+        mServiceContext.setPermission(
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_DENIED);
+    }
+
+    private ConnectionInfo getTestConnectionInfo() throws Exception {
+        return new ConnectionInfo(
+                IPPROTO_TCP,
+                new InetSocketAddress(InetAddresses.parseNumericAddress("1.2.3.4"), 1234),
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2.3.4.5"), 2345));
+    }
+
+    @Test
+    public void testGetConnectionOwnerUidPlatformVpn() throws Exception {
+        final int myUid = Process.myUid();
+        setupConnectionOwnerUidAsVpnApp(myUid, VpnManager.TYPE_VPN_PLATFORM);
+
+        assertEquals(INVALID_UID, mService.getConnectionOwnerUid(getTestConnectionInfo()));
+    }
+
+    @Test
+    public void testGetConnectionOwnerUidVpnServiceWrongUser() throws Exception {
+        final int myUid = Process.myUid();
+        setupConnectionOwnerUidAsVpnApp(myUid + 1, VpnManager.TYPE_VPN_SERVICE);
+
+        assertEquals(INVALID_UID, mService.getConnectionOwnerUid(getTestConnectionInfo()));
+    }
+
+    @Test
+    public void testGetConnectionOwnerUidVpnServiceDoesNotThrow() throws Exception {
+        final int myUid = Process.myUid();
+        setupConnectionOwnerUidAsVpnApp(myUid, VpnManager.TYPE_VPN_SERVICE);
+
+        assertEquals(42, mService.getConnectionOwnerUid(getTestConnectionInfo()));
+    }
+
+    @Test
+    public void testGetConnectionOwnerUidVpnServiceNetworkStackDoesNotThrow() throws Exception {
+        final int myUid = Process.myUid();
+        setupConnectionOwnerUid(myUid, VpnManager.TYPE_VPN_SERVICE);
+        mServiceContext.setPermission(NETWORK_STACK, PERMISSION_GRANTED);
+
+        assertEquals(42, mService.getConnectionOwnerUid(getTestConnectionInfo()));
+    }
+
+    @Test
+    public void testGetConnectionOwnerUidVpnServiceMainlineNetworkStackDoesNotThrow()
+            throws Exception {
+        final int myUid = Process.myUid();
+        setupConnectionOwnerUid(myUid, VpnManager.TYPE_VPN_SERVICE);
+        mServiceContext.setPermission(
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_GRANTED);
+
+        assertEquals(42, mService.getConnectionOwnerUid(getTestConnectionInfo()));
+    }
+
+    private static PackageInfo buildPackageInfo(boolean hasSystemPermission, int uid) {
+        final PackageInfo packageInfo = new PackageInfo();
+        if (hasSystemPermission) {
+            packageInfo.requestedPermissions = new String[] {
+                    CHANGE_NETWORK_STATE, CONNECTIVITY_USE_RESTRICTED_NETWORKS };
+            packageInfo.requestedPermissionsFlags = new int[] {
+                    REQUESTED_PERMISSION_GRANTED, REQUESTED_PERMISSION_GRANTED };
+        } else {
+            packageInfo.requestedPermissions = new String[0];
+        }
+        packageInfo.applicationInfo = new ApplicationInfo();
+        packageInfo.applicationInfo.privateFlags = 0;
+        packageInfo.applicationInfo.uid = UserHandle.getUid(UserHandle.USER_SYSTEM,
+                UserHandle.getAppId(uid));
+        return packageInfo;
+    }
+
+    @Test
+    public void testRegisterConnectivityDiagnosticsCallbackInvalidRequest() throws Exception {
+        final NetworkRequest request =
+                new NetworkRequest(
+                        new NetworkCapabilities(), TYPE_ETHERNET, 0, NetworkRequest.Type.NONE);
+        try {
+            mService.registerConnectivityDiagnosticsCallback(
+                    mConnectivityDiagnosticsCallback, request, mContext.getPackageName());
+            fail("registerConnectivityDiagnosticsCallback should throw on invalid NetworkRequest");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    private void assertRouteInfoParcelMatches(RouteInfo route, RouteInfoParcel parcel) {
+        assertEquals(route.getDestination().toString(), parcel.destination);
+        assertEquals(route.getInterface(), parcel.ifName);
+        assertEquals(route.getMtu(), parcel.mtu);
+
+        switch (route.getType()) {
+            case RouteInfo.RTN_UNICAST:
+                if (route.hasGateway()) {
+                    assertEquals(route.getGateway().getHostAddress(), parcel.nextHop);
+                } else {
+                    assertEquals(INetd.NEXTHOP_NONE, parcel.nextHop);
+                }
+                break;
+            case RouteInfo.RTN_UNREACHABLE:
+                assertEquals(INetd.NEXTHOP_UNREACHABLE, parcel.nextHop);
+                break;
+            case RouteInfo.RTN_THROW:
+                assertEquals(INetd.NEXTHOP_THROW, parcel.nextHop);
+                break;
+            default:
+                assertEquals(INetd.NEXTHOP_NONE, parcel.nextHop);
+                break;
+        }
+    }
+
+    private void assertRoutesAdded(int netId, RouteInfo... routes) throws Exception {
+        // TODO: add @JavaDerive(equals=true) to RouteInfoParcel, use eq() directly, and delete
+        // assertRouteInfoParcelMatches above.
+        ArgumentCaptor<RouteInfoParcel> captor = ArgumentCaptor.forClass(RouteInfoParcel.class);
+        verify(mMockNetd, times(routes.length)).networkAddRouteParcel(eq(netId), captor.capture());
+        for (int i = 0; i < routes.length; i++) {
+            assertRouteInfoParcelMatches(routes[i], captor.getAllValues().get(i));
+        }
+    }
+
+    private void assertRoutesRemoved(int netId, RouteInfo... routes) throws Exception {
+        ArgumentCaptor<RouteInfoParcel> captor = ArgumentCaptor.forClass(RouteInfoParcel.class);
+        verify(mMockNetd, times(routes.length)).networkRemoveRouteParcel(eq(netId),
+                captor.capture());
+        for (int i = 0; i < routes.length; i++) {
+            assertRouteInfoParcelMatches(routes[i], captor.getAllValues().get(i));
+        }
+    }
+
+    @Test
+    public void testRegisterUnregisterConnectivityDiagnosticsCallback() throws Exception {
+        final NetworkRequest wifiRequest =
+                new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build();
+        when(mConnectivityDiagnosticsCallback.asBinder()).thenReturn(mIBinder);
+
+        mService.registerConnectivityDiagnosticsCallback(
+                mConnectivityDiagnosticsCallback, wifiRequest, mContext.getPackageName());
+
+        // Block until all other events are done processing.
+        HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
+
+        verify(mIBinder).linkToDeath(any(ConnectivityDiagnosticsCallbackInfo.class), anyInt());
+        verify(mConnectivityDiagnosticsCallback).asBinder();
+        assertTrue(mService.mConnectivityDiagnosticsCallbacks.containsKey(mIBinder));
+
+        mService.unregisterConnectivityDiagnosticsCallback(mConnectivityDiagnosticsCallback);
+        verify(mIBinder, timeout(TIMEOUT_MS))
+                .unlinkToDeath(any(ConnectivityDiagnosticsCallbackInfo.class), anyInt());
+        assertFalse(mService.mConnectivityDiagnosticsCallbacks.containsKey(mIBinder));
+        verify(mConnectivityDiagnosticsCallback, atLeastOnce()).asBinder();
+    }
+
+    @Test
+    public void testRegisterDuplicateConnectivityDiagnosticsCallback() throws Exception {
+        final NetworkRequest wifiRequest =
+                new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build();
+        when(mConnectivityDiagnosticsCallback.asBinder()).thenReturn(mIBinder);
+
+        mService.registerConnectivityDiagnosticsCallback(
+                mConnectivityDiagnosticsCallback, wifiRequest, mContext.getPackageName());
+
+        // Block until all other events are done processing.
+        HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
+
+        verify(mIBinder).linkToDeath(any(ConnectivityDiagnosticsCallbackInfo.class), anyInt());
+        verify(mConnectivityDiagnosticsCallback).asBinder();
+        assertTrue(mService.mConnectivityDiagnosticsCallbacks.containsKey(mIBinder));
+
+        // Register the same callback again
+        mService.registerConnectivityDiagnosticsCallback(
+                mConnectivityDiagnosticsCallback, wifiRequest, mContext.getPackageName());
+
+        // Block until all other events are done processing.
+        HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
+
+        assertTrue(mService.mConnectivityDiagnosticsCallbacks.containsKey(mIBinder));
+    }
+
+    public NetworkAgentInfo fakeMobileNai(NetworkCapabilities nc) {
+        final NetworkCapabilities cellNc = new NetworkCapabilities.Builder(nc)
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        final NetworkInfo info = new NetworkInfo(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE,
+                ConnectivityManager.getNetworkTypeName(TYPE_MOBILE),
+                TelephonyManager.getNetworkTypeName(TelephonyManager.NETWORK_TYPE_LTE));
+        return fakeNai(cellNc, info);
+    }
+
+    private NetworkAgentInfo fakeWifiNai(NetworkCapabilities nc) {
+        final NetworkCapabilities wifiNc = new NetworkCapabilities.Builder(nc)
+                .addTransportType(TRANSPORT_WIFI).build();
+        final NetworkInfo info = new NetworkInfo(TYPE_WIFI, 0 /* subtype */,
+                ConnectivityManager.getNetworkTypeName(TYPE_WIFI), "" /* subtypeName */);
+        return fakeNai(wifiNc, info);
+    }
+
+    private NetworkAgentInfo fakeNai(NetworkCapabilities nc, NetworkInfo networkInfo) {
+        return new NetworkAgentInfo(null, new Network(NET_ID), networkInfo, new LinkProperties(),
+                nc, new NetworkScore.Builder().setLegacyInt(0).build(),
+                mServiceContext, null, new NetworkAgentConfig(), mService, null, null, 0,
+                INVALID_UID, TEST_LINGER_DELAY_MS, mQosCallbackTracker,
+                new ConnectivityService.Dependencies());
+    }
+
+    @Test
+    public void testCheckConnectivityDiagnosticsPermissionsNetworkStack() throws Exception {
+        final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities());
+
+        mServiceContext.setPermission(NETWORK_STACK, PERMISSION_GRANTED);
+        assertTrue(
+                "NetworkStack permission not applied",
+                mService.checkConnectivityDiagnosticsPermissions(
+                        Process.myPid(), Process.myUid(), naiWithoutUid,
+                        mContext.getOpPackageName()));
+    }
+
+    @Test
+    public void testCheckConnectivityDiagnosticsPermissionsWrongUidPackageName() throws Exception {
+        final int wrongUid = Process.myUid() + 1;
+
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setAdministratorUids(new int[] {wrongUid});
+        final NetworkAgentInfo naiWithUid = fakeWifiNai(nc);
+
+        mServiceContext.setPermission(NETWORK_STACK, PERMISSION_DENIED);
+
+        assertFalse(
+                "Mismatched uid/package name should not pass the location permission check",
+                mService.checkConnectivityDiagnosticsPermissions(
+                        Process.myPid() + 1, wrongUid, naiWithUid, mContext.getOpPackageName()));
+    }
+
+    private void verifyConnectivityDiagnosticsPermissionsWithNetworkAgentInfo(
+            NetworkAgentInfo info, boolean expectPermission) {
+        mServiceContext.setPermission(NETWORK_STACK, PERMISSION_DENIED);
+
+        assertEquals(
+                "Unexpected ConnDiags permission",
+                expectPermission,
+                mService.checkConnectivityDiagnosticsPermissions(
+                        Process.myPid(), Process.myUid(), info, mContext.getOpPackageName()));
+    }
+
+    @Test
+    public void testCheckConnectivityDiagnosticsPermissionsCellularNoLocationPermission()
+            throws Exception {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setAdministratorUids(new int[] {Process.myUid()});
+        final NetworkAgentInfo naiWithUid = fakeMobileNai(nc);
+
+        verifyConnectivityDiagnosticsPermissionsWithNetworkAgentInfo(naiWithUid,
+                true /* expectPermission */);
+    }
+
+    @Test
+    public void testCheckConnectivityDiagnosticsPermissionsWifiNoLocationPermission()
+            throws Exception {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setAdministratorUids(new int[] {Process.myUid()});
+        final NetworkAgentInfo naiWithUid = fakeWifiNai(nc);
+
+        verifyConnectivityDiagnosticsPermissionsWithNetworkAgentInfo(naiWithUid,
+                false /* expectPermission */);
+    }
+
+    @Test
+    public void testCheckConnectivityDiagnosticsPermissionsActiveVpn() throws Exception {
+        final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities());
+
+        mMockVpn.establishForMyUid();
+        assertUidRangesUpdatedForMyUid(true);
+
+        // Wait for networks to connect and broadcasts to be sent before removing permissions.
+        waitForIdle();
+        setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION,
+                Manifest.permission.ACCESS_FINE_LOCATION);
+
+        assertTrue(mMockVpn.setUnderlyingNetworks(new Network[] {naiWithoutUid.network}));
+        waitForIdle();
+        assertTrue(
+                "Active VPN permission not applied",
+                mService.checkConnectivityDiagnosticsPermissions(
+                        Process.myPid(), Process.myUid(), naiWithoutUid,
+                        mContext.getOpPackageName()));
+
+        assertTrue(mMockVpn.setUnderlyingNetworks(null));
+        waitForIdle();
+        assertFalse(
+                "VPN shouldn't receive callback on non-underlying network",
+                mService.checkConnectivityDiagnosticsPermissions(
+                        Process.myPid(), Process.myUid(), naiWithoutUid,
+                        mContext.getOpPackageName()));
+    }
+
+    @Test
+    public void testCheckConnectivityDiagnosticsPermissionsNetworkAdministrator() throws Exception {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setAdministratorUids(new int[] {Process.myUid()});
+        final NetworkAgentInfo naiWithUid = fakeMobileNai(nc);
+
+        setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION,
+                Manifest.permission.ACCESS_FINE_LOCATION);
+        mServiceContext.setPermission(NETWORK_STACK, PERMISSION_DENIED);
+
+        assertTrue(
+                "NetworkCapabilities administrator uid permission not applied",
+                mService.checkConnectivityDiagnosticsPermissions(
+                        Process.myPid(), Process.myUid(), naiWithUid, mContext.getOpPackageName()));
+    }
+
+    @Test
+    public void testCheckConnectivityDiagnosticsPermissionsFails() throws Exception {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setOwnerUid(Process.myUid());
+        nc.setAdministratorUids(new int[] {Process.myUid()});
+        final NetworkAgentInfo naiWithUid = fakeMobileNai(nc);
+
+        setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION,
+                Manifest.permission.ACCESS_FINE_LOCATION);
+        mServiceContext.setPermission(NETWORK_STACK, PERMISSION_DENIED);
+
+        // Use wrong pid and uid
+        assertFalse(
+                "Permissions allowed when they shouldn't be granted",
+                mService.checkConnectivityDiagnosticsPermissions(
+                        Process.myPid() + 1, Process.myUid() + 1, naiWithUid,
+                        mContext.getOpPackageName()));
+    }
+
+    @Test
+    public void testRegisterConnectivityDiagnosticsCallbackCallsOnConnectivityReport()
+            throws Exception {
+        // Set up the Network, which leads to a ConnectivityReport being cached for the network.
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(callback);
+        final LinkProperties linkProperties = new LinkProperties();
+        linkProperties.setInterfaceName(INTERFACE_NAME);
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, linkProperties);
+        mCellNetworkAgent.connect(true);
+        callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        callback.assertNoCallback();
+
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        when(mConnectivityDiagnosticsCallback.asBinder()).thenReturn(mIBinder);
+
+        mServiceContext.setPermission(NETWORK_STACK, PERMISSION_GRANTED);
+
+        mService.registerConnectivityDiagnosticsCallback(
+                mConnectivityDiagnosticsCallback, request, mContext.getPackageName());
+
+        // Block until all other events are done processing.
+        HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
+
+        verify(mConnectivityDiagnosticsCallback)
+                .onConnectivityReportAvailable(argThat(report -> {
+                    return INTERFACE_NAME.equals(report.getLinkProperties().getInterfaceName())
+                            && report.getNetworkCapabilities().hasTransport(TRANSPORT_CELLULAR);
+                }));
+    }
+
+    private void setUpConnectivityDiagnosticsCallback() throws Exception {
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        when(mConnectivityDiagnosticsCallback.asBinder()).thenReturn(mIBinder);
+
+        mServiceContext.setPermission(NETWORK_STACK, PERMISSION_GRANTED);
+
+        mService.registerConnectivityDiagnosticsCallback(
+                mConnectivityDiagnosticsCallback, request, mContext.getPackageName());
+
+        // Block until all other events are done processing.
+        HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
+
+        // Connect the cell agent verify that it notifies TestNetworkCallback that it is available
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerDefaultNetworkCallback(callback);
+
+        final NetworkCapabilities ncTemplate = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .setTransportInfo(new TestTransportInfo());
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(),
+                ncTemplate);
+        mCellNetworkAgent.connect(true);
+        callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        callback.assertNoCallback();
+    }
+
+    private boolean areConnDiagCapsRedacted(NetworkCapabilities nc) {
+        TestTransportInfo ti = (TestTransportInfo) nc.getTransportInfo();
+        return nc.getUids() == null
+                && nc.getAdministratorUids().length == 0
+                && nc.getOwnerUid() == Process.INVALID_UID
+                && getTestTransportInfo(nc).locationRedacted
+                && getTestTransportInfo(nc).localMacAddressRedacted
+                && getTestTransportInfo(nc).settingsRedacted;
+    }
+
+    @Test
+    public void testConnectivityDiagnosticsCallbackOnConnectivityReportAvailable()
+            throws Exception {
+        setUpConnectivityDiagnosticsCallback();
+
+        // Block until all other events are done processing.
+        HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
+
+        // Verify onConnectivityReport fired
+        verify(mConnectivityDiagnosticsCallback).onConnectivityReportAvailable(
+                argThat(report -> areConnDiagCapsRedacted(report.getNetworkCapabilities())));
+    }
+
+    @Test
+    public void testConnectivityDiagnosticsCallbackOnDataStallSuspected() throws Exception {
+        setUpConnectivityDiagnosticsCallback();
+
+        // Trigger notifyDataStallSuspected() on the INetworkMonitorCallbacks instance in the
+        // cellular network agent
+        mCellNetworkAgent.notifyDataStallSuspected();
+
+        // Block until all other events are done processing.
+        HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
+
+        // Verify onDataStallSuspected fired
+        verify(mConnectivityDiagnosticsCallback).onDataStallSuspected(
+                argThat(report -> areConnDiagCapsRedacted(report.getNetworkCapabilities())));
+    }
+
+    @Test
+    public void testConnectivityDiagnosticsCallbackOnConnectivityReported() throws Exception {
+        setUpConnectivityDiagnosticsCallback();
+
+        final Network n = mCellNetworkAgent.getNetwork();
+        final boolean hasConnectivity = true;
+        mService.reportNetworkConnectivity(n, hasConnectivity);
+
+        // Block until all other events are done processing.
+        HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
+
+        // Verify onNetworkConnectivityReported fired
+        verify(mConnectivityDiagnosticsCallback)
+                .onNetworkConnectivityReported(eq(n), eq(hasConnectivity));
+
+        final boolean noConnectivity = false;
+        mService.reportNetworkConnectivity(n, noConnectivity);
+
+        // Block until all other events are done processing.
+        HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
+
+        // Wait for onNetworkConnectivityReported to fire
+        verify(mConnectivityDiagnosticsCallback)
+                .onNetworkConnectivityReported(eq(n), eq(noConnectivity));
+    }
+
+    @Test
+    public void testRouteAddDeleteUpdate() throws Exception {
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        final TestNetworkCallback networkCallback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, networkCallback);
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        reset(mMockNetd);
+        mCellNetworkAgent.connect(false);
+        networkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        final int netId = mCellNetworkAgent.getNetwork().netId;
+
+        final String iface = "rmnet_data0";
+        final InetAddress gateway = InetAddress.getByName("fe80::5678");
+        RouteInfo direct = RouteInfo.makeHostRoute(gateway, iface);
+        RouteInfo rio1 = new RouteInfo(new IpPrefix("2001:db8:1::/48"), gateway, iface);
+        RouteInfo rio2 = new RouteInfo(new IpPrefix("2001:db8:2::/48"), gateway, iface);
+        RouteInfo defaultRoute = new RouteInfo((IpPrefix) null, gateway, iface);
+        RouteInfo defaultWithMtu = new RouteInfo(null, gateway, iface, RouteInfo.RTN_UNICAST,
+                                                 1280 /* mtu */);
+
+        // Send LinkProperties and check that we ask netd to add routes.
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(iface);
+        lp.addRoute(direct);
+        lp.addRoute(rio1);
+        lp.addRoute(defaultRoute);
+        mCellNetworkAgent.sendLinkProperties(lp);
+        networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, x -> x.getRoutes().size() == 3);
+
+        assertRoutesAdded(netId, direct, rio1, defaultRoute);
+        reset(mMockNetd);
+
+        // Send updated LinkProperties and check that we ask netd to add, remove, update routes.
+        assertTrue(lp.getRoutes().contains(defaultRoute));
+        lp.removeRoute(rio1);
+        lp.addRoute(rio2);
+        lp.addRoute(defaultWithMtu);
+        // Ensure adding the same route with a different MTU replaces the previous route.
+        assertFalse(lp.getRoutes().contains(defaultRoute));
+        assertTrue(lp.getRoutes().contains(defaultWithMtu));
+
+        mCellNetworkAgent.sendLinkProperties(lp);
+        networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
+                x -> x.getRoutes().contains(rio2));
+
+        assertRoutesRemoved(netId, rio1);
+        assertRoutesAdded(netId, rio2);
+
+        ArgumentCaptor<RouteInfoParcel> captor = ArgumentCaptor.forClass(RouteInfoParcel.class);
+        verify(mMockNetd).networkUpdateRouteParcel(eq(netId), captor.capture());
+        assertRouteInfoParcelMatches(defaultWithMtu, captor.getValue());
+
+
+        mCm.unregisterNetworkCallback(networkCallback);
+    }
+
+    @Test
+    public void testDumpDoesNotCrash() {
+        mServiceContext.setPermission(DUMP, PERMISSION_GRANTED);
+        // Filing a couple requests prior to testing the dump.
+        final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback();
+        final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest genericRequest = new NetworkRequest.Builder()
+                .clearCapabilities().build();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .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]);
+
+        assertFalse(stringWriter.toString().isEmpty());
+    }
+
+    @Test
+    public void testRequestsSortedByIdSortsCorrectly() {
+        final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback();
+        final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback();
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest genericRequest = new NetworkRequest.Builder()
+                .clearCapabilities().build();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI).build();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.registerNetworkCallback(genericRequest, genericNetworkCallback);
+        mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
+        mCm.registerNetworkCallback(cellRequest, cellNetworkCallback);
+        waitForIdle();
+
+        final NetworkRequestInfo[] nriOutput = mService.requestsSortedById();
+
+        assertTrue(nriOutput.length > 1);
+        for (int i = 0; i < nriOutput.length - 1; i++) {
+            final boolean isRequestIdInOrder =
+                    nriOutput[i].mRequests.get(0).requestId
+                            < nriOutput[i + 1].mRequests.get(0).requestId;
+            assertTrue(isRequestIdInOrder);
+        }
+    }
+
+    private void assertUidRangesUpdatedForMyUid(boolean add) throws Exception {
+        final int uid = Process.myUid();
+        assertVpnUidRangesUpdated(add, uidRangesForUids(uid), uid);
+    }
+
+    private void assertVpnUidRangesUpdated(boolean add, Set<UidRange> vpnRanges, int exemptUid)
+            throws Exception {
+        InOrder inOrder = inOrder(mMockNetd);
+        ArgumentCaptor<int[]> exemptUidCaptor = ArgumentCaptor.forClass(int[].class);
+
+        inOrder.verify(mMockNetd, times(1)).socketDestroy(eq(toUidRangeStableParcels(vpnRanges)),
+                exemptUidCaptor.capture());
+        assertContainsExactly(exemptUidCaptor.getValue(), Process.VPN_UID, exemptUid);
+
+        if (add) {
+            inOrder.verify(mMockNetd, times(1))
+                    .networkAddUidRanges(eq(mMockVpn.getNetwork().getNetId()),
+                    eq(toUidRangeStableParcels(vpnRanges)));
+        } else {
+            inOrder.verify(mMockNetd, times(1))
+                    .networkRemoveUidRanges(eq(mMockVpn.getNetwork().getNetId()),
+                    eq(toUidRangeStableParcels(vpnRanges)));
+        }
+
+        inOrder.verify(mMockNetd, times(1)).socketDestroy(eq(toUidRangeStableParcels(vpnRanges)),
+                exemptUidCaptor.capture());
+        assertContainsExactly(exemptUidCaptor.getValue(), Process.VPN_UID, exemptUid);
+    }
+
+    @Test
+    public void testVpnUidRangesUpdate() throws Exception {
+        // Set up a WiFi network without proxy.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        assertNull(mService.getProxyForNetwork(null));
+        assertNull(mCm.getDefaultProxy());
+
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
+        final UidRange vpnRange = PRIMARY_UIDRANGE;
+        final Set<UidRange> vpnRanges = Collections.singleton(vpnRange);
+        mMockVpn.establish(lp, VPN_UID, vpnRanges);
+        assertVpnUidRangesUpdated(true, vpnRanges, VPN_UID);
+        // VPN is connected but proxy is not set, so there is no need to send proxy broadcast.
+        verify(mProxyTracker, never()).sendProxyBroadcast();
+
+        // Update to new range which is old range minus APP1, i.e. only APP2
+        final Set<UidRange> newRanges = new HashSet<>(Arrays.asList(
+                new UidRange(vpnRange.start, APP1_UID - 1),
+                new UidRange(APP1_UID + 1, vpnRange.stop)));
+        mMockVpn.setUids(newRanges);
+        waitForIdle();
+
+        assertVpnUidRangesUpdated(true, newRanges, VPN_UID);
+        assertVpnUidRangesUpdated(false, vpnRanges, VPN_UID);
+
+        // Uid has changed but proxy is not set, so there is no need to send proxy broadcast.
+        verify(mProxyTracker, never()).sendProxyBroadcast();
+
+        final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888);
+        lp.setHttpProxy(testProxyInfo);
+        mMockVpn.sendLinkProperties(lp);
+        waitForIdle();
+        // Proxy is set, so send a proxy broadcast.
+        verify(mProxyTracker, times(1)).sendProxyBroadcast();
+        reset(mProxyTracker);
+
+        mMockVpn.setUids(vpnRanges);
+        waitForIdle();
+        // Uid has changed and proxy is already set, so send a proxy broadcast.
+        verify(mProxyTracker, times(1)).sendProxyBroadcast();
+        reset(mProxyTracker);
+
+        // Proxy is removed, send a proxy broadcast.
+        lp.setHttpProxy(null);
+        mMockVpn.sendLinkProperties(lp);
+        waitForIdle();
+        verify(mProxyTracker, times(1)).sendProxyBroadcast();
+        reset(mProxyTracker);
+
+        // Proxy is added in WiFi(default network), setDefaultProxy will be called.
+        final LinkProperties wifiLp = mCm.getLinkProperties(mWiFiNetworkAgent.getNetwork());
+        assertNotNull(wifiLp);
+        wifiLp.setHttpProxy(testProxyInfo);
+        mWiFiNetworkAgent.sendLinkProperties(wifiLp);
+        waitForIdle();
+        verify(mProxyTracker, times(1)).setDefaultProxy(eq(testProxyInfo));
+        reset(mProxyTracker);
+    }
+
+    @Test
+    public void testProxyBroadcastWillBeSentWhenVpnHasProxyAndConnects() throws Exception {
+        // Set up a WiFi network without proxy.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        assertNull(mService.getProxyForNetwork(null));
+        assertNull(mCm.getDefaultProxy());
+
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
+        final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888);
+        lp.setHttpProxy(testProxyInfo);
+        final UidRange vpnRange = PRIMARY_UIDRANGE;
+        final Set<UidRange> vpnRanges = Collections.singleton(vpnRange);
+        mMockVpn.setOwnerAndAdminUid(VPN_UID);
+        mMockVpn.registerAgent(false, vpnRanges, lp);
+        // In any case, the proxy broadcast won't be sent before VPN goes into CONNECTED state.
+        // Otherwise, the app that calls ConnectivityManager#getDefaultProxy() when it receives the
+        // proxy broadcast will get null.
+        verify(mProxyTracker, never()).sendProxyBroadcast();
+        mMockVpn.connect(true /* validated */, true /* hasInternet */, false /* isStrictMode */);
+        waitForIdle();
+        assertVpnUidRangesUpdated(true, vpnRanges, VPN_UID);
+        // Vpn is connected with proxy, so the proxy broadcast will be sent to inform the apps to
+        // update their proxy data.
+        verify(mProxyTracker, times(1)).sendProxyBroadcast();
+    }
+
+    @Test
+    public void testProxyBroadcastWillBeSentWhenTheProxyOfNonDefaultNetworkHasChanged()
+            throws Exception {
+        // Set up a CELLULAR network without proxy.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        assertNull(mService.getProxyForNetwork(null));
+        assertNull(mCm.getDefaultProxy());
+        // CELLULAR network should be the default network.
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // Set up a WiFi network without proxy.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        assertNull(mService.getProxyForNetwork(null));
+        assertNull(mCm.getDefaultProxy());
+        // WiFi network should be the default network.
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        // CELLULAR network is not the default network.
+        assertNotEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+        // CELLULAR network is not the system default network, but it might be a per-app default
+        // network. The proxy broadcast should be sent once its proxy has changed.
+        final LinkProperties cellularLp = new LinkProperties();
+        cellularLp.setInterfaceName(MOBILE_IFNAME);
+        final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888);
+        cellularLp.setHttpProxy(testProxyInfo);
+        mCellNetworkAgent.sendLinkProperties(cellularLp);
+        waitForIdle();
+        verify(mProxyTracker, times(1)).sendProxyBroadcast();
+    }
+
+    @Test
+    public void testInvalidRequestTypes() {
+        final int[] invalidReqTypeInts = new int[]{-1, NetworkRequest.Type.NONE.ordinal(),
+                NetworkRequest.Type.LISTEN.ordinal(), NetworkRequest.Type.values().length};
+        final NetworkCapabilities nc = new NetworkCapabilities().addTransportType(TRANSPORT_WIFI);
+
+        for (int reqTypeInt : invalidReqTypeInts) {
+            assertThrows("Expect throws for invalid request type " + reqTypeInt,
+                    IllegalArgumentException.class,
+                    () -> mService.requestNetwork(Process.INVALID_UID, nc, reqTypeInt, null, 0,
+                            null, ConnectivityManager.TYPE_NONE, NetworkCallback.FLAG_NONE,
+                            mContext.getPackageName(), getAttributionTag())
+            );
+        }
+    }
+
+    @Test
+    public void testKeepConnected() throws Exception {
+        setAlwaysOnNetworks(false);
+        registerDefaultNetworkCallbacks();
+        final TestNetworkCallback allNetworksCb = new TestNetworkCallback();
+        final NetworkRequest allNetworksRequest = new NetworkRequest.Builder().clearCapabilities()
+                .build();
+        mCm.registerNetworkCallback(allNetworksRequest, allNetworksCb);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true /* validated */);
+
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        allNetworksCb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true /* validated */);
+
+        mDefaultNetworkCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+        // While the default callback doesn't see the network before it's validated, the listen
+        // sees the network come up and validate later
+        allNetworksCb.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        allNetworksCb.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent);
+        allNetworksCb.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
+        allNetworksCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent,
+                TEST_LINGER_DELAY_MS * 2);
+
+        // The cell network has disconnected (see LOST above) because it was outscored and
+        // had no requests (see setAlwaysOnNetworks(false) above)
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        final NetworkScore score = new NetworkScore.Builder().setLegacyInt(30).build();
+        mCellNetworkAgent.setScore(score);
+        mCellNetworkAgent.connect(false /* validated */);
+
+        // The cell network gets torn down right away.
+        allNetworksCb.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        allNetworksCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent,
+                TEST_NASCENT_DELAY_MS * 2);
+        allNetworksCb.assertNoCallback();
+
+        // Now create a cell network with KEEP_CONNECTED_FOR_HANDOVER and make sure it's
+        // not disconnected immediately when outscored.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        final NetworkScore scoreKeepup = new NetworkScore.Builder().setLegacyInt(30)
+                .setKeepConnectedReason(KEEP_CONNECTED_FOR_HANDOVER).build();
+        mCellNetworkAgent.setScore(scoreKeepup);
+        mCellNetworkAgent.connect(true /* validated */);
+
+        allNetworksCb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.assertNoCallback();
+
+        mWiFiNetworkAgent.disconnect();
+
+        allNetworksCb.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+
+        // Reconnect a WiFi network and make sure the cell network is still not torn down.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true /* validated */);
+
+        allNetworksCb.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+
+        // Now remove the reason to keep connected and make sure the network lingers and is
+        // torn down.
+        mCellNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(30).build());
+        allNetworksCb.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent,
+                TEST_NASCENT_DELAY_MS * 2);
+        allNetworksCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent,
+                TEST_LINGER_DELAY_MS * 2);
+        mDefaultNetworkCallback.assertNoCallback();
+
+        mCm.unregisterNetworkCallback(allNetworksCb);
+        // mDefaultNetworkCallback will be unregistered by tearDown()
+    }
+
+    private class QosCallbackMockHelper {
+        @NonNull public final QosFilter mFilter;
+        @NonNull public final IQosCallback mCallback;
+        @NonNull public final TestNetworkAgentWrapper mAgentWrapper;
+        @NonNull private final List<IQosCallback> mCallbacks = new ArrayList();
+
+        QosCallbackMockHelper() throws Exception {
+            Log.d(TAG, "QosCallbackMockHelper: ");
+            mFilter = mock(QosFilter.class);
+
+            // Ensure the network is disconnected before anything else occurs
+            assertNull(mCellNetworkAgent);
+
+            mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+            mCellNetworkAgent.connect(true);
+
+            verifyActiveNetwork(TRANSPORT_CELLULAR);
+            waitForIdle();
+            final Network network = mCellNetworkAgent.getNetwork();
+
+            final Pair<IQosCallback, IBinder> pair = createQosCallback();
+            mCallback = pair.first;
+
+            when(mFilter.getNetwork()).thenReturn(network);
+            when(mFilter.validate()).thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE);
+            mAgentWrapper = mCellNetworkAgent;
+        }
+
+        void registerQosCallback(@NonNull final QosFilter filter,
+                @NonNull final IQosCallback callback) {
+            mCallbacks.add(callback);
+            final NetworkAgentInfo nai =
+                    mService.getNetworkAgentInfoForNetwork(filter.getNetwork());
+            mService.registerQosCallbackInternal(filter, callback, nai);
+        }
+
+        void tearDown() {
+            for (int i = 0; i < mCallbacks.size(); i++) {
+                mService.unregisterQosCallback(mCallbacks.get(i));
+            }
+        }
+    }
+
+    private Pair<IQosCallback, IBinder> createQosCallback() {
+        final IQosCallback callback = mock(IQosCallback.class);
+        final IBinder binder = mock(Binder.class);
+        when(callback.asBinder()).thenReturn(binder);
+        when(binder.isBinderAlive()).thenReturn(true);
+        return new Pair<>(callback, binder);
+    }
+
+
+    @Test
+    public void testQosCallbackRegistration() throws Exception {
+        mQosCallbackMockHelper = new QosCallbackMockHelper();
+        final NetworkAgentWrapper wrapper = mQosCallbackMockHelper.mAgentWrapper;
+
+        when(mQosCallbackMockHelper.mFilter.validate())
+                .thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE);
+        mQosCallbackMockHelper.registerQosCallback(
+                mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
+
+        final NetworkAgentWrapper.CallbackType.OnQosCallbackRegister cbRegister1 =
+                (NetworkAgentWrapper.CallbackType.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);
+        assertNotNull(cbUnregister);
+        assertEquals(registerCallbackId, cbUnregister.mQosCallbackId);
+        assertNull(wrapper.getCallbackHistory().poll(200, x -> true));
+    }
+
+    @Test
+    public void testQosCallbackNoRegistrationOnValidationError() throws Exception {
+        mQosCallbackMockHelper = new QosCallbackMockHelper();
+
+        when(mQosCallbackMockHelper.mFilter.validate())
+                .thenReturn(QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED);
+        mQosCallbackMockHelper.registerQosCallback(
+                mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
+        waitForIdle();
+        verify(mQosCallbackMockHelper.mCallback)
+                .onError(eq(QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED));
+    }
+
+    @Test
+    public void testQosCallbackAvailableAndLost() throws Exception {
+        mQosCallbackMockHelper = new QosCallbackMockHelper();
+        final int sessionId = 10;
+        final int qosCallbackId = 1;
+
+        when(mQosCallbackMockHelper.mFilter.validate())
+                .thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE);
+        mQosCallbackMockHelper.registerQosCallback(
+                mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
+        waitForIdle();
+
+        final EpsBearerQosSessionAttributes attributes = new EpsBearerQosSessionAttributes(
+                1, 2, 3, 4, 5, new ArrayList<>());
+        mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                .sendQosSessionAvailable(qosCallbackId, sessionId, attributes);
+        waitForIdle();
+
+        verify(mQosCallbackMockHelper.mCallback).onQosEpsBearerSessionAvailable(argThat(session ->
+                session.getSessionId() == sessionId
+                        && session.getSessionType() == QosSession.TYPE_EPS_BEARER), eq(attributes));
+
+        mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                .sendQosSessionLost(qosCallbackId, sessionId, QosSession.TYPE_EPS_BEARER);
+        waitForIdle();
+        verify(mQosCallbackMockHelper.mCallback).onQosSessionLost(argThat(session ->
+                session.getSessionId() == sessionId
+                        && session.getSessionType() == QosSession.TYPE_EPS_BEARER));
+    }
+
+    @Test
+    public void testNrQosCallbackAvailableAndLost() throws Exception {
+        mQosCallbackMockHelper = new QosCallbackMockHelper();
+        final int sessionId = 10;
+        final int qosCallbackId = 1;
+
+        when(mQosCallbackMockHelper.mFilter.validate())
+                .thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE);
+        mQosCallbackMockHelper.registerQosCallback(
+                mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
+        waitForIdle();
+
+        final NrQosSessionAttributes attributes = new NrQosSessionAttributes(
+                1, 2, 3, 4, 5, 6, 7, new ArrayList<>());
+        mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                .sendQosSessionAvailable(qosCallbackId, sessionId, attributes);
+        waitForIdle();
+
+        verify(mQosCallbackMockHelper.mCallback).onNrQosSessionAvailable(argThat(session ->
+                session.getSessionId() == sessionId
+                        && session.getSessionType() == QosSession.TYPE_NR_BEARER), eq(attributes));
+
+        mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+                .sendQosSessionLost(qosCallbackId, sessionId, QosSession.TYPE_NR_BEARER);
+        waitForIdle();
+        verify(mQosCallbackMockHelper.mCallback).onQosSessionLost(argThat(session ->
+                session.getSessionId() == sessionId
+                        && session.getSessionType() == QosSession.TYPE_NR_BEARER));
+    }
+
+    @Test
+    public void testQosCallbackTooManyRequests() throws Exception {
+        mQosCallbackMockHelper = new QosCallbackMockHelper();
+
+        when(mQosCallbackMockHelper.mFilter.validate())
+                .thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE);
+        for (int i = 0; i < 100; i++) {
+            final Pair<IQosCallback, IBinder> pair = createQosCallback();
+
+            try {
+                mQosCallbackMockHelper.registerQosCallback(
+                        mQosCallbackMockHelper.mFilter, pair.first);
+            } catch (ServiceSpecificException e) {
+                assertEquals(e.errorCode, ConnectivityManager.Errors.TOO_MANY_REQUESTS);
+                if (i < 50) {
+                    fail("TOO_MANY_REQUESTS thrown too early, the count is " + i);
+                }
+
+                // As long as there is at least 50 requests, it is safe to assume it works.
+                // Note: The count isn't being tested precisely against 100 because the counter
+                // is shared with request network.
+                return;
+            }
+        }
+        fail("TOO_MANY_REQUESTS never thrown");
+    }
+
+    private void mockGetApplicationInfo(@NonNull final String packageName, final int uid) {
+        mockGetApplicationInfo(packageName, uid, PRIMARY_USER_HANDLE);
+    }
+
+    private void mockGetApplicationInfo(@NonNull final String packageName, final int uid,
+            @NonNull final UserHandle user) {
+        final ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.uid = uid;
+        try {
+            when(mPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(), eq(user)))
+                    .thenReturn(applicationInfo);
+        } catch (Exception e) {
+            fail(e.getMessage());
+        }
+    }
+
+    private void mockGetApplicationInfoThrowsNameNotFound(@NonNull final String packageName,
+            @NonNull final UserHandle user)
+            throws Exception {
+        when(mPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(), eq(user)))
+                .thenThrow(new PackageManager.NameNotFoundException(packageName));
+    }
+
+    private void mockHasSystemFeature(@NonNull final String featureName, final boolean hasFeature) {
+        when(mPackageManager.hasSystemFeature(eq(featureName)))
+                .thenReturn(hasFeature);
+    }
+
+    private Range<Integer> getNriFirstUidRange(@NonNull final NetworkRequestInfo nri) {
+        return nri.mRequests.get(0).networkCapabilities.getUids().iterator().next();
+    }
+
+    private OemNetworkPreferences createDefaultOemNetworkPreferences(
+            @OemNetworkPreferences.OemNetworkPreference final int preference) {
+        // Arrange PackageManager mocks
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID);
+
+        // Build OemNetworkPreferences object
+        return new OemNetworkPreferences.Builder()
+                .addNetworkPreference(TEST_PACKAGE_NAME, preference)
+                .build();
+    }
+
+    @Test
+    public void testOemNetworkRequestFactoryPreferenceUninitializedThrowsError() {
+        @OemNetworkPreferences.OemNetworkPreference final int prefToTest =
+                OEM_NETWORK_PREFERENCE_UNINITIALIZED;
+
+        // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
+        assertThrows(IllegalArgumentException.class,
+                () -> mService.new OemNetworkRequestFactory()
+                        .createNrisFromOemNetworkPreferences(
+                                createDefaultOemNetworkPreferences(prefToTest)));
+    }
+
+    @Test
+    public void testOemNetworkRequestFactoryPreferenceOemPaid()
+            throws Exception {
+        // Expectations
+        final int expectedNumOfNris = 1;
+        final int expectedNumOfRequests = 3;
+
+        @OemNetworkPreferences.OemNetworkPreference final int prefToTest =
+                OEM_NETWORK_PREFERENCE_OEM_PAID;
+
+        // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
+        final ArraySet<NetworkRequestInfo> nris =
+                mService.new OemNetworkRequestFactory()
+                        .createNrisFromOemNetworkPreferences(
+                                createDefaultOemNetworkPreferences(prefToTest));
+        final NetworkRequestInfo nri = nris.iterator().next();
+        assertEquals(DEFAULT_NETWORK_PRIORITY_OEM, nri.getDefaultNetworkPriority());
+        final List<NetworkRequest> mRequests = nri.mRequests;
+        assertEquals(expectedNumOfNris, nris.size());
+        assertEquals(expectedNumOfRequests, mRequests.size());
+        assertTrue(mRequests.get(0).isListen());
+        assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_NOT_METERED));
+        assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_VALIDATED));
+        assertTrue(mRequests.get(1).isRequest());
+        assertTrue(mRequests.get(1).hasCapability(NET_CAPABILITY_OEM_PAID));
+        assertEquals(NetworkRequest.Type.TRACK_DEFAULT, mRequests.get(2).type);
+        assertTrue(mService.getDefaultRequest().networkCapabilities.equalsNetCapabilities(
+                mRequests.get(2).networkCapabilities));
+    }
+
+    @Test
+    public void testOemNetworkRequestFactoryPreferenceOemPaidNoFallback()
+            throws Exception {
+        // Expectations
+        final int expectedNumOfNris = 1;
+        final int expectedNumOfRequests = 2;
+
+        @OemNetworkPreferences.OemNetworkPreference final int prefToTest =
+                OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK;
+
+        // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
+        final ArraySet<NetworkRequestInfo> nris =
+                mService.new OemNetworkRequestFactory()
+                        .createNrisFromOemNetworkPreferences(
+                                createDefaultOemNetworkPreferences(prefToTest));
+        final NetworkRequestInfo nri = nris.iterator().next();
+        assertEquals(DEFAULT_NETWORK_PRIORITY_OEM, nri.getDefaultNetworkPriority());
+        final List<NetworkRequest> mRequests = nri.mRequests;
+        assertEquals(expectedNumOfNris, nris.size());
+        assertEquals(expectedNumOfRequests, mRequests.size());
+        assertTrue(mRequests.get(0).isListen());
+        assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_NOT_METERED));
+        assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_VALIDATED));
+        assertTrue(mRequests.get(1).isRequest());
+        assertTrue(mRequests.get(1).hasCapability(NET_CAPABILITY_OEM_PAID));
+    }
+
+    @Test
+    public void testOemNetworkRequestFactoryPreferenceOemPaidOnly()
+            throws Exception {
+        // Expectations
+        final int expectedNumOfNris = 1;
+        final int expectedNumOfRequests = 1;
+
+        @OemNetworkPreferences.OemNetworkPreference final int prefToTest =
+                OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY;
+
+        // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
+        final ArraySet<NetworkRequestInfo> nris =
+                mService.new OemNetworkRequestFactory()
+                        .createNrisFromOemNetworkPreferences(
+                                createDefaultOemNetworkPreferences(prefToTest));
+        final NetworkRequestInfo nri = nris.iterator().next();
+        assertEquals(DEFAULT_NETWORK_PRIORITY_OEM, nri.getDefaultNetworkPriority());
+        final List<NetworkRequest> mRequests = nri.mRequests;
+        assertEquals(expectedNumOfNris, nris.size());
+        assertEquals(expectedNumOfRequests, mRequests.size());
+        assertTrue(mRequests.get(0).isRequest());
+        assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_OEM_PAID));
+    }
+
+    @Test
+    public void testOemNetworkRequestFactoryPreferenceOemPrivateOnly()
+            throws Exception {
+        // Expectations
+        final int expectedNumOfNris = 1;
+        final int expectedNumOfRequests = 1;
+
+        @OemNetworkPreferences.OemNetworkPreference final int prefToTest =
+                OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY;
+
+        // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
+        final ArraySet<NetworkRequestInfo> nris =
+                mService.new OemNetworkRequestFactory()
+                        .createNrisFromOemNetworkPreferences(
+                                createDefaultOemNetworkPreferences(prefToTest));
+        final NetworkRequestInfo nri = nris.iterator().next();
+        assertEquals(DEFAULT_NETWORK_PRIORITY_OEM, nri.getDefaultNetworkPriority());
+        final List<NetworkRequest> mRequests = nri.mRequests;
+        assertEquals(expectedNumOfNris, nris.size());
+        assertEquals(expectedNumOfRequests, mRequests.size());
+        assertTrue(mRequests.get(0).isRequest());
+        assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_OEM_PRIVATE));
+        assertFalse(mRequests.get(0).hasCapability(NET_CAPABILITY_OEM_PAID));
+    }
+
+    @Test
+    public void testOemNetworkRequestFactoryCreatesCorrectNumOfNris()
+            throws Exception {
+        // Expectations
+        final int expectedNumOfNris = 2;
+
+        // Arrange PackageManager mocks
+        final String testPackageName2 = "com.google.apps.dialer";
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID);
+        mockGetApplicationInfo(testPackageName2, TEST_PACKAGE_UID);
+
+        // Build OemNetworkPreferences object
+        final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID;
+        final int testOemPref2 = OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK;
+        final OemNetworkPreferences pref = new OemNetworkPreferences.Builder()
+                .addNetworkPreference(TEST_PACKAGE_NAME, testOemPref)
+                .addNetworkPreference(testPackageName2, testOemPref2)
+                .build();
+
+        // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
+        final ArraySet<NetworkRequestInfo> nris =
+                mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(pref);
+
+        assertNotNull(nris);
+        assertEquals(expectedNumOfNris, nris.size());
+    }
+
+    @Test
+    public void testOemNetworkRequestFactoryMultiplePrefsCorrectlySetsUids()
+            throws Exception {
+        // Arrange PackageManager mocks
+        final String testPackageName2 = "com.google.apps.dialer";
+        final int testPackageNameUid2 = 456;
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID);
+        mockGetApplicationInfo(testPackageName2, testPackageNameUid2);
+
+        // Build OemNetworkPreferences object
+        final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID;
+        final int testOemPref2 = OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK;
+        final OemNetworkPreferences pref = new OemNetworkPreferences.Builder()
+                .addNetworkPreference(TEST_PACKAGE_NAME, testOemPref)
+                .addNetworkPreference(testPackageName2, testOemPref2)
+                .build();
+
+        // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
+        final List<NetworkRequestInfo> nris =
+                new ArrayList<>(
+                        mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(
+                                pref));
+
+        // Sort by uid to access nris by index
+        nris.sort(Comparator.comparingInt(nri -> getNriFirstUidRange(nri).getLower()));
+        assertEquals(TEST_PACKAGE_UID, (int) getNriFirstUidRange(nris.get(0)).getLower());
+        assertEquals(TEST_PACKAGE_UID, (int) getNriFirstUidRange(nris.get(0)).getUpper());
+        assertEquals(testPackageNameUid2, (int) getNriFirstUidRange(nris.get(1)).getLower());
+        assertEquals(testPackageNameUid2, (int) getNriFirstUidRange(nris.get(1)).getUpper());
+    }
+
+    @Test
+    public void testOemNetworkRequestFactoryMultipleUsersSetsUids()
+            throws Exception {
+        // Arrange users
+        final int secondUserTestPackageUid = UserHandle.getUid(SECONDARY_USER, TEST_PACKAGE_UID);
+        final int thirdUserTestPackageUid = UserHandle.getUid(TERTIARY_USER, TEST_PACKAGE_UID);
+        when(mUserManager.getUserHandles(anyBoolean())).thenReturn(
+                Arrays.asList(PRIMARY_USER_HANDLE, SECONDARY_USER_HANDLE, TERTIARY_USER_HANDLE));
+
+        // Arrange PackageManager mocks testing for users who have and don't have a package.
+        mockGetApplicationInfoThrowsNameNotFound(TEST_PACKAGE_NAME, PRIMARY_USER_HANDLE);
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, secondUserTestPackageUid, SECONDARY_USER_HANDLE);
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, thirdUserTestPackageUid, TERTIARY_USER_HANDLE);
+
+        // Build OemNetworkPreferences object
+        final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID;
+        final OemNetworkPreferences pref = new OemNetworkPreferences.Builder()
+                .addNetworkPreference(TEST_PACKAGE_NAME, testOemPref)
+                .build();
+
+        // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
+        final List<NetworkRequestInfo> nris =
+                new ArrayList<>(
+                        mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(
+                                pref));
+
+        // UIDs for users with installed packages should be present.
+        // Three users exist, but only two have the test package installed.
+        final int expectedUidSize = 2;
+        final List<Range<Integer>> uids =
+                new ArrayList<>(nris.get(0).mRequests.get(0).networkCapabilities.getUids());
+        assertEquals(expectedUidSize, uids.size());
+
+        // Sort by uid to access nris by index
+        uids.sort(Comparator.comparingInt(uid -> uid.getLower()));
+        assertEquals(secondUserTestPackageUid, (int) uids.get(0).getLower());
+        assertEquals(secondUserTestPackageUid, (int) uids.get(0).getUpper());
+        assertEquals(thirdUserTestPackageUid, (int) uids.get(1).getLower());
+        assertEquals(thirdUserTestPackageUid, (int) uids.get(1).getUpper());
+    }
+
+    @Test
+    public void testOemNetworkRequestFactoryAddsPackagesToCorrectPreference()
+            throws Exception {
+        // Expectations
+        final int expectedNumOfNris = 1;
+        final int expectedNumOfAppUids = 2;
+
+        // Arrange PackageManager mocks
+        final String testPackageName2 = "com.google.apps.dialer";
+        final int testPackageNameUid2 = 456;
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID);
+        mockGetApplicationInfo(testPackageName2, testPackageNameUid2);
+
+        // Build OemNetworkPreferences object
+        final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID;
+        final OemNetworkPreferences pref = new OemNetworkPreferences.Builder()
+                .addNetworkPreference(TEST_PACKAGE_NAME, testOemPref)
+                .addNetworkPreference(testPackageName2, testOemPref)
+                .build();
+
+        // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences()
+        final ArraySet<NetworkRequestInfo> nris =
+                mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(pref);
+
+        assertEquals(expectedNumOfNris, nris.size());
+        assertEquals(expectedNumOfAppUids,
+                nris.iterator().next().mRequests.get(0).networkCapabilities.getUids().size());
+    }
+
+    @Test
+    public void testSetOemNetworkPreferenceNullListenerAndPrefParamThrowsNpe() {
+        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true);
+
+        // Act on ConnectivityService.setOemNetworkPreference()
+        assertThrows(NullPointerException.class,
+                () -> mService.setOemNetworkPreference(
+                        null,
+                        null));
+    }
+
+    @Test
+    public void testSetOemNetworkPreferenceFailsForNonAutomotive()
+            throws Exception {
+        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, false);
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY;
+
+        // Act on ConnectivityService.setOemNetworkPreference()
+        assertThrows(UnsupportedOperationException.class,
+                () -> mService.setOemNetworkPreference(
+                        createDefaultOemNetworkPreferences(networkPref),
+                        null));
+    }
+
+    @Test
+    public void testSetOemNetworkPreferenceFailsForTestRequestWithoutPermission() {
+        // Calling setOemNetworkPreference() with a test pref requires the permission
+        // MANAGE_TEST_NETWORKS.
+        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, false);
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_TEST;
+
+        // Act on ConnectivityService.setOemNetworkPreference()
+        assertThrows(SecurityException.class,
+                () -> mService.setOemNetworkPreference(
+                        createDefaultOemNetworkPreferences(networkPref),
+                        null));
+    }
+
+    @Test
+    public void testSetOemNetworkPreferenceFailsForInvalidTestRequest() {
+        assertSetOemNetworkPreferenceFailsForInvalidTestRequest(OEM_NETWORK_PREFERENCE_TEST);
+    }
+
+    @Test
+    public void testSetOemNetworkPreferenceFailsForInvalidTestOnlyRequest() {
+        assertSetOemNetworkPreferenceFailsForInvalidTestRequest(OEM_NETWORK_PREFERENCE_TEST_ONLY);
+    }
+
+    private void assertSetOemNetworkPreferenceFailsForInvalidTestRequest(
+            @OemNetworkPreferences.OemNetworkPreference final int oemNetworkPreferenceForTest) {
+        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true);
+        final String secondPackage = "does.not.matter";
+
+        // A valid test request would only have a single mapping.
+        final OemNetworkPreferences pref = new OemNetworkPreferences.Builder()
+                .addNetworkPreference(TEST_PACKAGE_NAME, oemNetworkPreferenceForTest)
+                .addNetworkPreference(secondPackage, oemNetworkPreferenceForTest)
+                .build();
+
+        // Act on ConnectivityService.setOemNetworkPreference()
+        assertThrows(IllegalArgumentException.class,
+                () -> mService.setOemNetworkPreference(pref, null));
+    }
+
+    private void setOemNetworkPreferenceAgentConnected(final int transportType,
+            final boolean connectAgent) throws Exception {
+        switch(transportType) {
+            // Corresponds to a metered cellular network. Will be used for the default network.
+            case TRANSPORT_CELLULAR:
+                if (!connectAgent) {
+                    mCellNetworkAgent.disconnect();
+                    break;
+                }
+                mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+                mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED);
+                mCellNetworkAgent.connect(true);
+                break;
+            // Corresponds to a restricted ethernet network with OEM_PAID/OEM_PRIVATE.
+            case TRANSPORT_ETHERNET:
+                if (!connectAgent) {
+                    stopOemManagedNetwork();
+                    break;
+                }
+                startOemManagedNetwork(true);
+                break;
+            // Corresponds to unmetered Wi-Fi.
+            case TRANSPORT_WIFI:
+                if (!connectAgent) {
+                    mWiFiNetworkAgent.disconnect();
+                    break;
+                }
+                mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+                mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+                mWiFiNetworkAgent.connect(true);
+                break;
+            default:
+                throw new AssertionError("Unsupported transport type passed in.");
+
+        }
+        waitForIdle();
+    }
+
+    private void startOemManagedNetwork(final boolean isOemPaid) throws Exception {
+        mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
+        mEthernetNetworkAgent.addCapability(
+                isOemPaid ? NET_CAPABILITY_OEM_PAID : NET_CAPABILITY_OEM_PRIVATE);
+        mEthernetNetworkAgent.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        mEthernetNetworkAgent.connect(true);
+    }
+
+    private void stopOemManagedNetwork() {
+        mEthernetNetworkAgent.disconnect();
+        waitForIdle();
+    }
+
+    private void verifyMultipleDefaultNetworksTracksCorrectly(
+            final int expectedOemRequestsSize,
+            @NonNull final Network expectedDefaultNetwork,
+            @NonNull final Network expectedPerAppNetwork) {
+        // The current test setup assumes two tracked default network requests; one for the default
+        // network and the other for the OEM network preference being tested. This will be validated
+        // each time to confirm it doesn't change under test.
+        final int expectedDefaultNetworkRequestsSize = 2;
+        assertEquals(expectedDefaultNetworkRequestsSize, mService.mDefaultNetworkRequests.size());
+        for (final NetworkRequestInfo defaultRequest : mService.mDefaultNetworkRequests) {
+            final Network defaultNetwork = defaultRequest.getSatisfier() == null
+                    ? null : defaultRequest.getSatisfier().network();
+            // If this is the default request.
+            if (defaultRequest == mService.mDefaultRequest) {
+                assertEquals(
+                        expectedDefaultNetwork,
+                        defaultNetwork);
+                // Make sure this value doesn't change.
+                assertEquals(1, defaultRequest.mRequests.size());
+                continue;
+            }
+            assertEquals(expectedPerAppNetwork, defaultNetwork);
+            assertEquals(expectedOemRequestsSize, defaultRequest.mRequests.size());
+        }
+        verifyMultipleDefaultCallbacks(expectedDefaultNetwork, expectedPerAppNetwork);
+    }
+
+    /**
+     * Verify default callbacks for 'available' fire as expected. This will only run if
+     * registerDefaultNetworkCallbacks() was executed prior and will only be different if the
+     * setOemNetworkPreference() per-app API was used for the current process.
+     * @param expectedSystemDefault the expected network for the system default.
+     * @param expectedPerAppDefault the expected network for the current process's default.
+     */
+    private void verifyMultipleDefaultCallbacks(
+            @NonNull final Network expectedSystemDefault,
+            @NonNull final Network expectedPerAppDefault) {
+        if (null != mSystemDefaultNetworkCallback && null != expectedSystemDefault
+                && mService.mNoServiceNetwork.network() != expectedSystemDefault) {
+            // getLastAvailableNetwork() is used as this method can be called successively with
+            // the same network to validate therefore expectAvailableThenValidatedCallbacks
+            // can't be used.
+            assertEquals(mSystemDefaultNetworkCallback.getLastAvailableNetwork(),
+                    expectedSystemDefault);
+        }
+        if (null != mDefaultNetworkCallback && null != expectedPerAppDefault
+                && mService.mNoServiceNetwork.network() != expectedPerAppDefault) {
+            assertEquals(mDefaultNetworkCallback.getLastAvailableNetwork(),
+                    expectedPerAppDefault);
+        }
+    }
+
+    private void registerDefaultNetworkCallbacks() {
+        if (mSystemDefaultNetworkCallback != null || mDefaultNetworkCallback != null
+                || mProfileDefaultNetworkCallback != null
+                || mTestPackageDefaultNetworkCallback != null) {
+            throw new IllegalStateException("Default network callbacks already registered");
+        }
+
+        // Using Manifest.permission.NETWORK_SETTINGS for registerSystemDefaultNetworkCallback()
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+        mSystemDefaultNetworkCallback = new TestNetworkCallback();
+        mDefaultNetworkCallback = new TestNetworkCallback();
+        mProfileDefaultNetworkCallback = new TestNetworkCallback();
+        mTestPackageDefaultNetworkCallback = new TestNetworkCallback();
+        mCm.registerSystemDefaultNetworkCallback(mSystemDefaultNetworkCallback,
+                new Handler(ConnectivityThread.getInstanceLooper()));
+        mCm.registerDefaultNetworkCallback(mDefaultNetworkCallback);
+        registerDefaultNetworkCallbackAsUid(mProfileDefaultNetworkCallback,
+                TEST_WORK_PROFILE_APP_UID);
+        registerDefaultNetworkCallbackAsUid(mTestPackageDefaultNetworkCallback, TEST_PACKAGE_UID);
+        // TODO: test using ConnectivityManager#registerDefaultNetworkCallbackAsUid as well.
+        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED);
+    }
+
+    private void unregisterDefaultNetworkCallbacks() {
+        if (null != mDefaultNetworkCallback) {
+            mCm.unregisterNetworkCallback(mDefaultNetworkCallback);
+        }
+        if (null != mSystemDefaultNetworkCallback) {
+            mCm.unregisterNetworkCallback(mSystemDefaultNetworkCallback);
+        }
+        if (null != mProfileDefaultNetworkCallback) {
+            mCm.unregisterNetworkCallback(mProfileDefaultNetworkCallback);
+        }
+        if (null != mTestPackageDefaultNetworkCallback) {
+            mCm.unregisterNetworkCallback(mTestPackageDefaultNetworkCallback);
+        }
+    }
+
+    private void setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest(
+            @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup)
+            throws Exception {
+        final int testPackageNameUid = TEST_PACKAGE_UID;
+        final String testPackageName = "per.app.defaults.package";
+        setupMultipleDefaultNetworksForOemNetworkPreferenceTest(
+                networkPrefToSetup, testPackageNameUid, testPackageName);
+    }
+
+    private void setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(
+            @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup)
+            throws Exception {
+        final int testPackageNameUid = Process.myUid();
+        final String testPackageName = "per.app.defaults.package";
+        setupMultipleDefaultNetworksForOemNetworkPreferenceTest(
+                networkPrefToSetup, testPackageNameUid, testPackageName);
+    }
+
+    private void setupMultipleDefaultNetworksForOemNetworkPreferenceTest(
+            @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup,
+            final int testPackageUid, @NonNull final String testPackageName) throws Exception {
+        // Only the default request should be included at start.
+        assertEquals(1, mService.mDefaultNetworkRequests.size());
+
+        final UidRangeParcel[] uidRanges =
+                toUidRangeStableParcels(uidRangesForUids(testPackageUid));
+        setupSetOemNetworkPreferenceForPreferenceTest(
+                networkPrefToSetup, uidRanges, testPackageName);
+    }
+
+    private void setupSetOemNetworkPreferenceForPreferenceTest(
+            @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup,
+            @NonNull final UidRangeParcel[] uidRanges,
+            @NonNull final String testPackageName) throws Exception {
+        setupSetOemNetworkPreferenceForPreferenceTest(
+                networkPrefToSetup, uidRanges, testPackageName, true);
+    }
+
+    private void setupSetOemNetworkPreferenceForPreferenceTest(
+            @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup,
+            @NonNull final UidRangeParcel[] uidRanges,
+            @NonNull final String testPackageName,
+            final boolean hasAutomotiveFeature) throws Exception {
+        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, hasAutomotiveFeature);
+
+        // These tests work off a single UID therefore using 'start' is valid.
+        mockGetApplicationInfo(testPackageName, uidRanges[0].start);
+
+        setOemNetworkPreference(networkPrefToSetup, testPackageName);
+    }
+
+    private void setOemNetworkPreference(final int networkPrefToSetup,
+            @NonNull final String... testPackageNames)
+            throws Exception {
+        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true);
+
+        // Build OemNetworkPreferences object
+        final OemNetworkPreferences.Builder builder = new OemNetworkPreferences.Builder();
+        for (final String packageName : testPackageNames) {
+            builder.addNetworkPreference(packageName, networkPrefToSetup);
+        }
+        final OemNetworkPreferences pref = builder.build();
+
+        // Act on ConnectivityService.setOemNetworkPreference()
+        final TestOemListenerCallback oemPrefListener = new TestOemListenerCallback();
+        mService.setOemNetworkPreference(pref, oemPrefListener);
+
+        // Verify call returned successfully
+        oemPrefListener.expectOnComplete();
+    }
+
+    private static class TestOemListenerCallback implements IOnCompleteListener {
+        final CompletableFuture<Object> mDone = new CompletableFuture<>();
+
+        @Override
+        public void onComplete() {
+            mDone.complete(new Object());
+        }
+
+        void expectOnComplete() {
+            try {
+                mDone.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            } catch (TimeoutException e) {
+                fail("Expected onComplete() not received after " + TIMEOUT_MS + " ms");
+            } catch (Exception e) {
+                fail(e.getMessage());
+            }
+        }
+
+        @Override
+        public IBinder asBinder() {
+            return null;
+        }
+    }
+
+    @Test
+    public void testMultiDefaultGetActiveNetworkIsCorrect() throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY;
+        final int expectedOemPrefRequestSize = 1;
+        registerDefaultNetworkCallbacks();
+
+        // Setup the test process to use networkPref for their default network.
+        setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref);
+
+        // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID.
+        // The active network for the default should be null at this point as this is a retricted
+        // network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                null,
+                mEthernetNetworkAgent.getNetwork());
+
+        // Verify that the active network is correct
+        verifyActiveNetwork(TRANSPORT_ETHERNET);
+        // default NCs will be unregistered in tearDown
+    }
+
+    @Test
+    public void testMultiDefaultIsActiveNetworkMeteredIsCorrect() throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY;
+        final int expectedOemPrefRequestSize = 1;
+        registerDefaultNetworkCallbacks();
+
+        // Setup the test process to use networkPref for their default network.
+        setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref);
+
+        // Returns true by default when no network is available.
+        assertTrue(mCm.isActiveNetworkMetered());
+
+        // Connect to an unmetered restricted network that will only be available to the OEM pref.
+        mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
+        mEthernetNetworkAgent.addCapability(NET_CAPABILITY_OEM_PAID);
+        mEthernetNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mEthernetNetworkAgent.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        mEthernetNetworkAgent.connect(true);
+        waitForIdle();
+
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                null,
+                mEthernetNetworkAgent.getNetwork());
+
+        assertFalse(mCm.isActiveNetworkMetered());
+        // default NCs will be unregistered in tearDown
+    }
+
+    @Test
+    public void testPerAppDefaultRegisterDefaultNetworkCallback() throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY;
+        final int expectedOemPrefRequestSize = 1;
+        final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback();
+
+        // Register the default network callback before the pref is already set. This means that
+        // the policy will be applied to the callback on setOemNetworkPreference().
+        mCm.registerDefaultNetworkCallback(defaultNetworkCallback);
+        defaultNetworkCallback.assertNoCallback();
+
+        final TestNetworkCallback otherUidDefaultCallback = new TestNetworkCallback();
+        withPermission(NETWORK_SETTINGS, () ->
+                mCm.registerDefaultNetworkCallbackForUid(TEST_PACKAGE_UID, otherUidDefaultCallback,
+                        new Handler(ConnectivityThread.getInstanceLooper())));
+
+        // Setup the test process to use networkPref for their default network.
+        setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref);
+
+        // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID.
+        // The active nai for the default is null at this point as this is a restricted network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                null,
+                mEthernetNetworkAgent.getNetwork());
+
+        // At this point with a restricted network used, the available callback should trigger.
+        defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent);
+        assertEquals(defaultNetworkCallback.getLastAvailableNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+        otherUidDefaultCallback.assertNoCallback();
+
+        // Now bring down the default network which should trigger a LOST callback.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false);
+
+        // At this point, with no network is available, the lost callback should trigger
+        defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent);
+        otherUidDefaultCallback.assertNoCallback();
+
+        // Confirm we can unregister without issues.
+        mCm.unregisterNetworkCallback(defaultNetworkCallback);
+        mCm.unregisterNetworkCallback(otherUidDefaultCallback);
+    }
+
+    @Test
+    public void testPerAppDefaultRegisterDefaultNetworkCallbackAfterPrefSet() throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY;
+        final int expectedOemPrefRequestSize = 1;
+        final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback();
+
+        // Setup the test process to use networkPref for their default network.
+        setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref);
+
+        // Register the default network callback after the pref is already set. This means that
+        // the policy will be applied to the callback on requestNetwork().
+        mCm.registerDefaultNetworkCallback(defaultNetworkCallback);
+        defaultNetworkCallback.assertNoCallback();
+
+        final TestNetworkCallback otherUidDefaultCallback = new TestNetworkCallback();
+        withPermission(NETWORK_SETTINGS, () ->
+                mCm.registerDefaultNetworkCallbackForUid(TEST_PACKAGE_UID, otherUidDefaultCallback,
+                        new Handler(ConnectivityThread.getInstanceLooper())));
+
+        // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID.
+        // The active nai for the default is null at this point as this is a restricted network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                null,
+                mEthernetNetworkAgent.getNetwork());
+
+        // At this point with a restricted network used, the available callback should trigger
+        defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent);
+        assertEquals(defaultNetworkCallback.getLastAvailableNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+        otherUidDefaultCallback.assertNoCallback();
+
+        // Now bring down the default network which should trigger a LOST callback.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false);
+        otherUidDefaultCallback.assertNoCallback();
+
+        // At this point, with no network is available, the lost callback should trigger
+        defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent);
+        otherUidDefaultCallback.assertNoCallback();
+
+        // Confirm we can unregister without issues.
+        mCm.unregisterNetworkCallback(defaultNetworkCallback);
+        mCm.unregisterNetworkCallback(otherUidDefaultCallback);
+    }
+
+    @Test
+    public void testPerAppDefaultRegisterDefaultNetworkCallbackDoesNotFire() throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY;
+        final int expectedOemPrefRequestSize = 1;
+        final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback();
+        final int userId = UserHandle.getUserId(Process.myUid());
+
+        mCm.registerDefaultNetworkCallback(defaultNetworkCallback);
+        defaultNetworkCallback.assertNoCallback();
+
+        final TestNetworkCallback otherUidDefaultCallback = new TestNetworkCallback();
+        withPermission(NETWORK_SETTINGS, () ->
+                mCm.registerDefaultNetworkCallbackForUid(TEST_PACKAGE_UID, otherUidDefaultCallback,
+                        new Handler(ConnectivityThread.getInstanceLooper())));
+
+        // Setup a process different than the test process to use the default network. This means
+        // that the defaultNetworkCallback won't be tracked by the per-app policy.
+        setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest(networkPref);
+
+        // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID.
+        // The active nai for the default is null at this point as this is a restricted network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                null,
+                mEthernetNetworkAgent.getNetwork());
+
+        // As this callback does not have access to the OEM_PAID network, it will not fire.
+        defaultNetworkCallback.assertNoCallback();
+        assertDefaultNetworkCapabilities(userId /* no networks */);
+
+        // The other UID does have access, and gets a callback.
+        otherUidDefaultCallback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent);
+
+        // Bring up unrestricted cellular. This should now satisfy the default network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+
+        // At this point with an unrestricted network used, the available callback should trigger
+        // The other UID is unaffected and remains on the paid network.
+        defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        assertEquals(defaultNetworkCallback.getLastAvailableNetwork(),
+                mCellNetworkAgent.getNetwork());
+        assertDefaultNetworkCapabilities(userId, mCellNetworkAgent);
+        otherUidDefaultCallback.assertNoCallback();
+
+        // Now bring down the per-app network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false);
+
+        // Since the callback didn't use the per-app network, only the other UID gets a callback.
+        // Because the preference specifies no fallback, it does not switch to cellular.
+        defaultNetworkCallback.assertNoCallback();
+        otherUidDefaultCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent);
+
+        // Now bring down the default network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false);
+
+        // As this callback was tracking the default, this should now trigger.
+        defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        otherUidDefaultCallback.assertNoCallback();
+
+        // Confirm we can unregister without issues.
+        mCm.unregisterNetworkCallback(defaultNetworkCallback);
+        mCm.unregisterNetworkCallback(otherUidDefaultCallback);
+    }
+
+    /**
+     * This method assumes that the same uidRanges input will be used to verify that dependencies
+     * are called as expected.
+     */
+    private void verifySetOemNetworkPreferenceForPreference(
+            @NonNull final UidRangeParcel[] uidRanges,
+            final int addUidRangesNetId,
+            final int addUidRangesTimes,
+            final int removeUidRangesNetId,
+            final int removeUidRangesTimes,
+            final boolean shouldDestroyNetwork) throws RemoteException {
+        verifySetOemNetworkPreferenceForPreference(uidRanges, uidRanges,
+                addUidRangesNetId, addUidRangesTimes, removeUidRangesNetId, removeUidRangesTimes,
+                shouldDestroyNetwork);
+    }
+
+    private void verifySetOemNetworkPreferenceForPreference(
+            @NonNull final UidRangeParcel[] addedUidRanges,
+            @NonNull final UidRangeParcel[] removedUidRanges,
+            final int addUidRangesNetId,
+            final int addUidRangesTimes,
+            final int removeUidRangesNetId,
+            final int removeUidRangesTimes,
+            final boolean shouldDestroyNetwork) throws RemoteException {
+        final boolean useAnyIdForAdd = OEM_PREF_ANY_NET_ID == addUidRangesNetId;
+        final boolean useAnyIdForRemove = OEM_PREF_ANY_NET_ID == removeUidRangesNetId;
+
+        // Validate netd.
+        verify(mMockNetd, times(addUidRangesTimes))
+                .networkAddUidRanges(
+                        (useAnyIdForAdd ? anyInt() : eq(addUidRangesNetId)), eq(addedUidRanges));
+        verify(mMockNetd, times(removeUidRangesTimes))
+                .networkRemoveUidRanges(
+                        (useAnyIdForRemove ? anyInt() : eq(removeUidRangesNetId)),
+                        eq(removedUidRanges));
+        if (shouldDestroyNetwork) {
+            verify(mMockNetd, times(1))
+                    .networkDestroy((useAnyIdForRemove ? anyInt() : eq(removeUidRangesNetId)));
+        }
+        reset(mMockNetd);
+    }
+
+    /**
+     * Test the tracked default requests allows test requests without standard setup.
+     */
+    @Test
+    public void testSetOemNetworkPreferenceAllowsValidTestRequestWithoutChecks() throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference int networkPref =
+                OEM_NETWORK_PREFERENCE_TEST;
+        validateSetOemNetworkPreferenceAllowsValidTestPrefRequest(networkPref);
+    }
+
+    /**
+     * Test the tracked default requests allows test only requests without standard setup.
+     */
+    @Test
+    public void testSetOemNetworkPreferenceAllowsValidTestOnlyRequestWithoutChecks()
+            throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference int networkPref =
+                OEM_NETWORK_PREFERENCE_TEST_ONLY;
+        validateSetOemNetworkPreferenceAllowsValidTestPrefRequest(networkPref);
+    }
+
+    private void validateSetOemNetworkPreferenceAllowsValidTestPrefRequest(int networkPref)
+            throws Exception {
+        // The caller must have the MANAGE_TEST_NETWORKS permission.
+        final int testPackageUid = 123;
+        final String validTestPackageName = "does.not.matter";
+        final UidRangeParcel[] uidRanges =
+                toUidRangeStableParcels(uidRangesForUids(testPackageUid));
+        mServiceContext.setPermission(
+                Manifest.permission.MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
+
+        // Put the system into a state in which setOemNetworkPreference() would normally fail. This
+        // will confirm that a valid test request can bypass these checks.
+        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, false);
+        mServiceContext.setPermission(
+                Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE, PERMISSION_DENIED);
+
+        // Validate the starting requests only includes the system default request.
+        assertEquals(1, mService.mDefaultNetworkRequests.size());
+
+        // Add an OEM default network request to track.
+        setupSetOemNetworkPreferenceForPreferenceTest(
+                networkPref, uidRanges, validTestPackageName,
+                false /* hasAutomotiveFeature */);
+
+        // Two requests should now exist; the system default and the test request.
+        assertEquals(2, mService.mDefaultNetworkRequests.size());
+    }
+
+    /**
+     * Test the tracked default requests clear previous OEM requests on setOemNetworkPreference().
+     */
+    @Test
+    public void testSetOemNetworkPreferenceClearPreviousOemValues() throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID;
+        final int testPackageUid = 123;
+        final String testPackageName = "com.google.apps.contacts";
+        final UidRangeParcel[] uidRanges =
+                toUidRangeStableParcels(uidRangesForUids(testPackageUid));
+
+        // Validate the starting requests only includes the system default request.
+        assertEquals(1, mService.mDefaultNetworkRequests.size());
+
+        // Add an OEM default network request to track.
+        setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, testPackageName);
+
+        // Two requests should exist, one for the fallback and one for the pref.
+        assertEquals(2, mService.mDefaultNetworkRequests.size());
+
+        networkPref = OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY;
+        setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, testPackageName);
+
+        // Two requests should still exist validating the previous per-app request was replaced.
+        assertEquals(2, mService.mDefaultNetworkRequests.size());
+    }
+
+    /**
+     * Test network priority for preference OEM_NETWORK_PREFERENCE_OEM_PAID in the following order:
+     * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID -> fallback
+     */
+    @Test
+    public void testMultilayerForPreferenceOemPaidEvaluatesCorrectly()
+            throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID;
+
+        // Arrange PackageManager mocks
+        final UidRangeParcel[] uidRanges =
+                toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID));
+        setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME);
+
+        // Verify the starting state. No networks should be connected.
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Test lowest to highest priority requests.
+        // Bring up metered cellular. This will satisfy the fallback network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mEthernetNetworkAgent.getNetwork().netId, 1 /* times */,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Bring up unmetered Wi-Fi. This will satisfy NET_CAPABILITY_NOT_METERED.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mWiFiNetworkAgent.getNetwork().netId, 1 /* times */,
+                mEthernetNetworkAgent.getNetwork().netId, 1 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Disconnecting OEM_PAID should have no effect as it is lower in priority then unmetered.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false);
+        // netd should not be called as default networks haven't changed.
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Disconnecting unmetered should put PANS on lowest priority fallback request.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                mWiFiNetworkAgent.getNetwork().netId, 0 /* times */,
+                true /* shouldDestroyNetwork */);
+
+        // Disconnecting the fallback network should result in no connectivity.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                mCellNetworkAgent.getNetwork().netId, 0 /* times */,
+                true /* shouldDestroyNetwork */);
+    }
+
+    /**
+     * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK in the following order:
+     * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID
+     */
+    @Test
+    public void testMultilayerForPreferenceOemPaidNoFallbackEvaluatesCorrectly()
+            throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK;
+
+        // Arrange PackageManager mocks
+        final UidRangeParcel[] uidRanges =
+                toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID));
+        setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME);
+
+        // Verify the starting state. This preference doesn't support using the fallback network
+        // therefore should be on the disconnected network as it has no networks to connect to.
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mService.mNoServiceNetwork.network.getNetId(), 1 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Test lowest to highest priority requests.
+        // Bring up metered cellular. This will satisfy the fallback network.
+        // This preference should not use this network as it doesn't support fallback usage.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mEthernetNetworkAgent.getNetwork().netId, 1 /* times */,
+                mService.mNoServiceNetwork.network.getNetId(), 1 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Bring up unmetered Wi-Fi. This will satisfy NET_CAPABILITY_NOT_METERED.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mWiFiNetworkAgent.getNetwork().netId, 1 /* times */,
+                mEthernetNetworkAgent.getNetwork().netId, 1 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Disconnecting unmetered should put PANS on OEM_PAID.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mEthernetNetworkAgent.getNetwork().netId, 1 /* times */,
+                mWiFiNetworkAgent.getNetwork().netId, 0 /* times */,
+                true /* shouldDestroyNetwork */);
+
+        // Disconnecting OEM_PAID should result in no connectivity.
+        // OEM_PAID_NO_FALLBACK not supporting a fallback now uses the disconnected network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mService.mNoServiceNetwork.network.getNetId(), 1 /* times */,
+                mEthernetNetworkAgent.getNetwork().netId, 0 /* times */,
+                true /* shouldDestroyNetwork */);
+    }
+
+    /**
+     * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY in the following order:
+     * NET_CAPABILITY_OEM_PAID
+     * This preference should only apply to OEM_PAID networks.
+     */
+    @Test
+    public void testMultilayerForPreferenceOemPaidOnlyEvaluatesCorrectly()
+            throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY;
+
+        // Arrange PackageManager mocks
+        final UidRangeParcel[] uidRanges =
+                toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID));
+        setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME);
+
+        // Verify the starting state. This preference doesn't support using the fallback network
+        // therefore should be on the disconnected network as it has no networks to connect to.
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mService.mNoServiceNetwork.network.getNetId(), 1 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Bring up metered cellular. This should not apply to this preference.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Bring up unmetered Wi-Fi. This should not apply to this preference.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mEthernetNetworkAgent.getNetwork().netId, 1 /* times */,
+                mService.mNoServiceNetwork.network.getNetId(), 1 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Disconnecting OEM_PAID should result in no connectivity.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mService.mNoServiceNetwork.network.getNetId(), 1 /* times */,
+                mEthernetNetworkAgent.getNetwork().netId, 0 /* times */,
+                true /* shouldDestroyNetwork */);
+    }
+
+    /**
+     * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY in the following order:
+     * NET_CAPABILITY_OEM_PRIVATE
+     * This preference should only apply to OEM_PRIVATE networks.
+     */
+    @Test
+    public void testMultilayerForPreferenceOemPrivateOnlyEvaluatesCorrectly()
+            throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY;
+
+        // Arrange PackageManager mocks
+        final UidRangeParcel[] uidRanges =
+                toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID));
+        setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME);
+
+        // Verify the starting state. This preference doesn't support using the fallback network
+        // therefore should be on the disconnected network as it has no networks to connect to.
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mService.mNoServiceNetwork.network.getNetId(), 1 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Bring up metered cellular. This should not apply to this preference.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Bring up unmetered Wi-Fi. This should not apply to this preference.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Bring up ethernet with OEM_PRIVATE. This will satisfy NET_CAPABILITY_OEM_PRIVATE.
+        startOemManagedNetwork(false);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mEthernetNetworkAgent.getNetwork().netId, 1 /* times */,
+                mService.mNoServiceNetwork.network.getNetId(), 1 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Disconnecting OEM_PRIVATE should result in no connectivity.
+        stopOemManagedNetwork();
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mService.mNoServiceNetwork.network.getNetId(), 1 /* times */,
+                mEthernetNetworkAgent.getNetwork().netId, 0 /* times */,
+                true /* shouldDestroyNetwork */);
+    }
+
+    @Test
+    public void testMultilayerForMultipleUsersEvaluatesCorrectly()
+            throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID;
+
+        // Arrange users
+        final int secondUser = 10;
+        final UserHandle secondUserHandle = new UserHandle(secondUser);
+        when(mUserManager.getUserHandles(anyBoolean())).thenReturn(
+                Arrays.asList(PRIMARY_USER_HANDLE, secondUserHandle));
+
+        // Arrange PackageManager mocks
+        final int secondUserTestPackageUid = UserHandle.getUid(secondUser, TEST_PACKAGE_UID);
+        final UidRangeParcel[] uidRanges =
+                toUidRangeStableParcels(
+                        uidRangesForUids(TEST_PACKAGE_UID, secondUserTestPackageUid));
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, secondUserTestPackageUid, secondUserHandle);
+        setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME);
+
+        // Verify the starting state. No networks should be connected.
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Test that we correctly add the expected values for multiple users.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Test that we correctly remove the expected values for multiple users.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false);
+        verifySetOemNetworkPreferenceForPreference(uidRanges,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                mCellNetworkAgent.getNetwork().netId, 0 /* times */,
+                true /* shouldDestroyNetwork */);
+    }
+
+    @Test
+    public void testMultilayerForBroadcastedUsersEvaluatesCorrectly()
+            throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID;
+
+        // Arrange users
+        final int secondUser = 10;
+        final UserHandle secondUserHandle = new UserHandle(secondUser);
+        when(mUserManager.getUserHandles(anyBoolean())).thenReturn(
+                Arrays.asList(PRIMARY_USER_HANDLE));
+
+        // Arrange PackageManager mocks
+        final int secondUserTestPackageUid = UserHandle.getUid(secondUser, TEST_PACKAGE_UID);
+        final UidRangeParcel[] uidRangesSingleUser =
+                toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID));
+        final UidRangeParcel[] uidRangesBothUsers =
+                toUidRangeStableParcels(
+                        uidRangesForUids(TEST_PACKAGE_UID, secondUserTestPackageUid));
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, secondUserTestPackageUid, secondUserHandle);
+        setupSetOemNetworkPreferenceForPreferenceTest(
+                networkPref, uidRangesSingleUser, TEST_PACKAGE_NAME);
+
+        // Verify the starting state. No networks should be connected.
+        verifySetOemNetworkPreferenceForPreference(uidRangesSingleUser,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Test that we correctly add the expected values for multiple users.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        verifySetOemNetworkPreferenceForPreference(uidRangesSingleUser,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Send a broadcast indicating a user was added.
+        when(mUserManager.getUserHandles(anyBoolean())).thenReturn(
+                Arrays.asList(PRIMARY_USER_HANDLE, secondUserHandle));
+        final Intent addedIntent = new Intent(ACTION_USER_ADDED);
+        addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(secondUser));
+        processBroadcast(addedIntent);
+
+        // Test that we correctly add values for all users and remove for the single user.
+        verifySetOemNetworkPreferenceForPreference(uidRangesBothUsers, uidRangesSingleUser,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Send a broadcast indicating a user was removed.
+        when(mUserManager.getUserHandles(anyBoolean())).thenReturn(
+                Arrays.asList(PRIMARY_USER_HANDLE));
+        final Intent removedIntent = new Intent(ACTION_USER_REMOVED);
+        removedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(secondUser));
+        processBroadcast(removedIntent);
+
+        // Test that we correctly add values for the single user and remove for the all users.
+        verifySetOemNetworkPreferenceForPreference(uidRangesSingleUser, uidRangesBothUsers,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                false /* shouldDestroyNetwork */);
+    }
+
+    @Test
+    public void testMultilayerForPackageChangesEvaluatesCorrectly()
+            throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID;
+        final String packageScheme = "package:";
+
+        // Arrange PackageManager mocks
+        final String packageToInstall = "package.to.install";
+        final int packageToInstallUid = 81387;
+        final UidRangeParcel[] uidRangesSinglePackage =
+                toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID));
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID);
+        mockGetApplicationInfoThrowsNameNotFound(packageToInstall, PRIMARY_USER_HANDLE);
+        setOemNetworkPreference(networkPref, TEST_PACKAGE_NAME, packageToInstall);
+        grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid(), packageToInstall);
+
+        // Verify the starting state. No networks should be connected.
+        verifySetOemNetworkPreferenceForPreference(uidRangesSinglePackage,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Test that we correctly add the expected values for installed packages.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        verifySetOemNetworkPreferenceForPreference(uidRangesSinglePackage,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                OEM_PREF_ANY_NET_ID, 0 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Set the system to recognize the package to be installed
+        mockGetApplicationInfo(packageToInstall, packageToInstallUid);
+        final UidRangeParcel[] uidRangesAllPackages =
+                toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID, packageToInstallUid));
+
+        // Send a broadcast indicating a package was installed.
+        final Intent addedIntent = new Intent(ACTION_PACKAGE_ADDED);
+        addedIntent.setData(Uri.parse(packageScheme + packageToInstall));
+        processBroadcast(addedIntent);
+
+        // Test the single package is removed and the combined packages are added.
+        verifySetOemNetworkPreferenceForPreference(uidRangesAllPackages, uidRangesSinglePackage,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Set the system to no longer recognize the package to be installed
+        mockGetApplicationInfoThrowsNameNotFound(packageToInstall, PRIMARY_USER_HANDLE);
+
+        // Send a broadcast indicating a package was removed.
+        final Intent removedIntent = new Intent(ACTION_PACKAGE_REMOVED);
+        removedIntent.setData(Uri.parse(packageScheme + packageToInstall));
+        processBroadcast(removedIntent);
+
+        // Test the combined packages are removed and the single package is added.
+        verifySetOemNetworkPreferenceForPreference(uidRangesSinglePackage, uidRangesAllPackages,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                false /* shouldDestroyNetwork */);
+
+        // Set the system to change the installed package's uid
+        final int replacedTestPackageUid = TEST_PACKAGE_UID + 1;
+        mockGetApplicationInfo(TEST_PACKAGE_NAME, replacedTestPackageUid);
+        final UidRangeParcel[] uidRangesReplacedPackage =
+                toUidRangeStableParcels(uidRangesForUids(replacedTestPackageUid));
+
+        // Send a broadcast indicating a package was replaced.
+        final Intent replacedIntent = new Intent(ACTION_PACKAGE_REPLACED);
+        replacedIntent.setData(Uri.parse(packageScheme + TEST_PACKAGE_NAME));
+        processBroadcast(replacedIntent);
+
+        // Test the original uid is removed and is replaced with the new uid.
+        verifySetOemNetworkPreferenceForPreference(uidRangesReplacedPackage, uidRangesSinglePackage,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                mCellNetworkAgent.getNetwork().netId, 1 /* times */,
+                false /* shouldDestroyNetwork */);
+    }
+
+    /**
+     * Test network priority for preference OEM_NETWORK_PREFERENCE_OEM_PAID in the following order:
+     * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID -> fallback
+     */
+    @Test
+    public void testMultipleDefaultNetworksTracksOemNetworkPreferenceOemPaidCorrectly()
+            throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID;
+        setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref);
+        final int expectedDefaultRequestSize = 2;
+        final int expectedOemPrefRequestSize = 3;
+        registerDefaultNetworkCallbacks();
+
+        // The fallback as well as the OEM preference should now be tracked.
+        assertEquals(expectedDefaultRequestSize, mService.mDefaultNetworkRequests.size());
+
+        // Test lowest to highest priority requests.
+        // Bring up metered cellular. This will satisfy the fallback network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mCellNetworkAgent.getNetwork());
+
+        // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+
+        // Bring up unmetered Wi-Fi. This will satisfy NET_CAPABILITY_NOT_METERED.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mWiFiNetworkAgent.getNetwork(),
+                mWiFiNetworkAgent.getNetwork());
+
+        // Disconnecting unmetered Wi-Fi will put the pref on OEM_PAID and fallback on cellular.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+
+        // Disconnecting cellular should keep OEM network on OEM_PAID and fallback will be null.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                null,
+                mEthernetNetworkAgent.getNetwork());
+
+        // Disconnecting OEM_PAID will put both on null as it is the last network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                null,
+                null);
+
+        // default callbacks will be unregistered in tearDown
+    }
+
+    @Test
+    public void testNetworkFactoryRequestsWithMultilayerRequest()
+            throws Exception {
+        // First use OEM_PAID preference to create a multi-layer request : 1. listen for
+        // unmetered, 2. request network with cap OEM_PAID, 3, request the default network for
+        // fallback.
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID;
+        setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref);
+
+        final HandlerThread handlerThread = new HandlerThread("MockFactory");
+        handlerThread.start();
+        NetworkCapabilities internetFilter = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        final MockNetworkFactory internetFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "internetFactory", internetFilter, mCsHandlerThread);
+        internetFactory.setScoreFilter(40);
+        internetFactory.register();
+        // Default internet request only. The unmetered request is never sent to factories (it's a
+        // LISTEN, not requestable). The 3rd (fallback) request in OEM_PAID NRI is TRACK_DEFAULT
+        // which is also not sent to factories. Finally, the OEM_PAID request doesn't match the
+        // internetFactory filter.
+        internetFactory.expectRequestAdds(1);
+        internetFactory.assertRequestCountEquals(1);
+
+        NetworkCapabilities oemPaidFilter = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_OEM_PAID)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+                .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        final MockNetworkFactory oemPaidFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "oemPaidFactory", oemPaidFilter, mCsHandlerThread);
+        oemPaidFactory.setScoreFilter(40);
+        oemPaidFactory.register();
+        oemPaidFactory.expectRequestAdd(); // Because nobody satisfies the request
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        // A network connected that satisfies the default internet request. For the OEM_PAID
+        // preference, this is not as good as an OEM_PAID network, so even if the score of
+        // the network is better than the factory announced, it still should try to bring up
+        // the network.
+        expectNoRequestChanged(oemPaidFactory);
+        oemPaidFactory.assertRequestCountEquals(1);
+        // The internet factory however is outscored, and should lose its requests.
+        internetFactory.expectRequestRemove();
+        internetFactory.assertRequestCountEquals(0);
+
+        final NetworkCapabilities oemPaidNc = new NetworkCapabilities();
+        oemPaidNc.addCapability(NET_CAPABILITY_OEM_PAID);
+        oemPaidNc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        final TestNetworkAgentWrapper oemPaidAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR,
+                new LinkProperties(), oemPaidNc);
+        oemPaidAgent.connect(true);
+
+        // The oemPaidAgent has score 50/cell transport, so it beats what the oemPaidFactory can
+        // provide, therefore it loses the request.
+        oemPaidFactory.expectRequestRemove();
+        oemPaidFactory.assertRequestCountEquals(0);
+        expectNoRequestChanged(internetFactory);
+        internetFactory.assertRequestCountEquals(0);
+
+        oemPaidAgent.setScore(new NetworkScore.Builder().setLegacyInt(20).setExiting(true).build());
+        // Now the that the agent is weak, the oemPaidFactory can beat the existing network for the
+        // OEM_PAID request. The internet factory however can't beat a network that has OEM_PAID
+        // for the preference request, so it doesn't see the request.
+        oemPaidFactory.expectRequestAdd();
+        oemPaidFactory.assertRequestCountEquals(1);
+        expectNoRequestChanged(internetFactory);
+        internetFactory.assertRequestCountEquals(0);
+
+        mCellNetworkAgent.disconnect();
+        // The network satisfying the default internet request has disconnected, so the
+        // internetFactory sees the default request again. However there is a network with OEM_PAID
+        // connected, so the 2nd OEM_PAID req is already satisfied, so the oemPaidFactory doesn't
+        // care about networks that don't have OEM_PAID.
+        expectNoRequestChanged(oemPaidFactory);
+        oemPaidFactory.assertRequestCountEquals(1);
+        internetFactory.expectRequestAdd();
+        internetFactory.assertRequestCountEquals(1);
+
+        // Cell connects again, still with score 50. Back to the previous state.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        expectNoRequestChanged(oemPaidFactory);
+        oemPaidFactory.assertRequestCountEquals(1);
+        internetFactory.expectRequestRemove();
+        internetFactory.assertRequestCountEquals(0);
+
+        // Create a request that holds the upcoming wifi network.
+        final TestNetworkCallback wifiCallback = new TestNetworkCallback();
+        mCm.requestNetwork(new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(),
+                wifiCallback);
+
+        // Now WiFi connects and it's unmetered, but it's weaker than cell.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+        mWiFiNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(30).setExiting(true)
+                .build()); // Not the best Internet network, but unmetered
+        mWiFiNetworkAgent.connect(true);
+
+        // The OEM_PAID preference prefers an unmetered network to an OEM_PAID network, so
+        // the oemPaidFactory can't beat wifi no matter how high its score.
+        oemPaidFactory.expectRequestRemove();
+        expectNoRequestChanged(internetFactory);
+
+        mCellNetworkAgent.disconnect();
+        // Now that the best internet network (cell, with its 50 score compared to 30 for WiFi
+        // at this point), the default internet request is satisfied by a network worse than
+        // the internetFactory announced, so it gets the request. However, there is still an
+        // unmetered network, so the oemPaidNetworkFactory still can't beat this.
+        expectNoRequestChanged(oemPaidFactory);
+        internetFactory.expectRequestAdd();
+        mCm.unregisterNetworkCallback(wifiCallback);
+    }
+
+    /**
+     * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK in the following order:
+     * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID
+     */
+    @Test
+    public void testMultipleDefaultNetworksTracksOemNetworkPreferenceOemPaidNoFallbackCorrectly()
+            throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK;
+        setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref);
+        final int expectedDefaultRequestSize = 2;
+        final int expectedOemPrefRequestSize = 2;
+        registerDefaultNetworkCallbacks();
+
+        // The fallback as well as the OEM preference should now be tracked.
+        assertEquals(expectedDefaultRequestSize, mService.mDefaultNetworkRequests.size());
+
+        // Test lowest to highest priority requests.
+        // Bring up metered cellular. This will satisfy the fallback network but not the pref.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mService.mNoServiceNetwork.network());
+
+        // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+
+        // Bring up unmetered Wi-Fi. This will satisfy NET_CAPABILITY_NOT_METERED.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mWiFiNetworkAgent.getNetwork(),
+                mWiFiNetworkAgent.getNetwork());
+
+        // Disconnecting unmetered Wi-Fi will put the OEM pref on OEM_PAID and fallback on cellular.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+
+        // Disconnecting cellular should keep OEM network on OEM_PAID and fallback will be null.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                null,
+                mEthernetNetworkAgent.getNetwork());
+
+        // Disconnecting OEM_PAID puts the fallback on null and the pref on the disconnected net.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                null,
+                mService.mNoServiceNetwork.network());
+
+        // default callbacks will be unregistered in tearDown
+    }
+
+    /**
+     * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY in the following order:
+     * NET_CAPABILITY_OEM_PAID
+     * This preference should only apply to OEM_PAID networks.
+     */
+    @Test
+    public void testMultipleDefaultNetworksTracksOemNetworkPreferenceOemPaidOnlyCorrectly()
+            throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY;
+        setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref);
+        final int expectedDefaultRequestSize = 2;
+        final int expectedOemPrefRequestSize = 1;
+        registerDefaultNetworkCallbacks();
+
+        // The fallback as well as the OEM preference should now be tracked.
+        assertEquals(expectedDefaultRequestSize, mService.mDefaultNetworkRequests.size());
+
+        // Test lowest to highest priority requests.
+        // Bring up metered cellular. This will satisfy the fallback network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mService.mNoServiceNetwork.network());
+
+        // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+
+        // Bring up unmetered Wi-Fi. The OEM network shouldn't change, the fallback will take Wi-Fi.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mWiFiNetworkAgent.getNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+
+        // Disconnecting unmetered Wi-Fi shouldn't change the OEM network with fallback on cellular.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+
+        // Disconnecting OEM_PAID will keep the fallback on cellular and nothing for OEM_PAID.
+        // OEM_PAID_ONLY not supporting a fallback now uses the disconnected network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mService.mNoServiceNetwork.network());
+
+        // Disconnecting cellular will put the fallback on null and the pref on disconnected.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                null,
+                mService.mNoServiceNetwork.network());
+
+        // default callbacks will be unregistered in tearDown
+    }
+
+    /**
+     * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY in the following order:
+     * NET_CAPABILITY_OEM_PRIVATE
+     * This preference should only apply to OEM_PRIVATE networks.
+     */
+    @Test
+    public void testMultipleDefaultNetworksTracksOemNetworkPreferenceOemPrivateOnlyCorrectly()
+            throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY;
+        setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref);
+        final int expectedDefaultRequestSize = 2;
+        final int expectedOemPrefRequestSize = 1;
+        registerDefaultNetworkCallbacks();
+
+        // The fallback as well as the OEM preference should now be tracked.
+        assertEquals(expectedDefaultRequestSize, mService.mDefaultNetworkRequests.size());
+
+        // Test lowest to highest priority requests.
+        // Bring up metered cellular. This will satisfy the fallback network.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mService.mNoServiceNetwork.network());
+
+        // Bring up ethernet with OEM_PRIVATE. This will satisfy NET_CAPABILITY_OEM_PRIVATE.
+        startOemManagedNetwork(false);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+
+        // Bring up unmetered Wi-Fi. The OEM network shouldn't change, the fallback will take Wi-Fi.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mWiFiNetworkAgent.getNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+
+        // Disconnecting unmetered Wi-Fi shouldn't change the OEM network with fallback on cellular.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mEthernetNetworkAgent.getNetwork());
+
+        // Disconnecting OEM_PRIVATE will keep the fallback on cellular.
+        // OEM_PRIVATE_ONLY not supporting a fallback now uses to the disconnected network.
+        stopOemManagedNetwork();
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                mCellNetworkAgent.getNetwork(),
+                mService.mNoServiceNetwork.network());
+
+        // Disconnecting cellular will put the fallback on null and pref on disconnected.
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false);
+        verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize,
+                null,
+                mService.mNoServiceNetwork.network());
+
+        // default callbacks will be unregistered in tearDown
+    }
+
+    @Test
+    public void testCapabilityWithOemNetworkPreference() throws Exception {
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY;
+        setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest(networkPref);
+        registerDefaultNetworkCallbacks();
+
+        setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+
+        mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
+        mSystemDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc ->
+                nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+        mDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc ->
+                nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+
+        // default callbacks will be unregistered in tearDown
+    }
+
+    @Test
+    public void testSetOemNetworkPreferenceLogsRequest() throws Exception {
+        mServiceContext.setPermission(DUMP, PERMISSION_GRANTED);
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID;
+        final StringWriter stringWriter = new StringWriter();
+        final String logIdentifier = "UPDATE INITIATED: OemNetworkPreferences";
+        final Pattern pattern = Pattern.compile(logIdentifier);
+
+        final int expectedNumLogs = 2;
+        final UidRangeParcel[] uidRanges =
+                toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID));
+
+        // Call twice to generate two logs.
+        setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME);
+        setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME);
+        mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), new String[0]);
+
+        final String dumpOutput = stringWriter.toString();
+        final Matcher matcher = pattern.matcher(dumpOutput);
+        int count = 0;
+        while (matcher.find()) {
+            count++;
+        }
+        assertEquals(expectedNumLogs, count);
+    }
+
+    @Test
+    public void testGetAllNetworkStateSnapshots() throws Exception {
+        verifyNoNetwork();
+
+        // Setup test cellular network with specified LinkProperties and NetworkCapabilities,
+        // verify the content of the snapshot matches.
+        final LinkProperties cellLp = new LinkProperties();
+        final LinkAddress myIpv4Addr = new LinkAddress(InetAddress.getByName("192.0.2.129"), 25);
+        final LinkAddress myIpv6Addr = new LinkAddress(InetAddress.getByName("2001:db8::1"), 64);
+        cellLp.setInterfaceName("test01");
+        cellLp.addLinkAddress(myIpv4Addr);
+        cellLp.addLinkAddress(myIpv6Addr);
+        cellLp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234")));
+        cellLp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254")));
+        cellLp.addRoute(new RouteInfo(myIpv4Addr, null));
+        cellLp.addRoute(new RouteInfo(myIpv6Addr, null));
+        final NetworkCapabilities cellNcTemplate = new NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_MMS).build();
+
+        final TestNetworkCallback cellCb = new TestNetworkCallback();
+        mCm.requestNetwork(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build(),
+                cellCb);
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp, cellNcTemplate);
+        mCellNetworkAgent.connect(true);
+        cellCb.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        List<NetworkStateSnapshot> snapshots = mCm.getAllNetworkStateSnapshots();
+        assertLength(1, snapshots);
+
+        // Compose the expected cellular snapshot for verification.
+        final NetworkCapabilities cellNc =
+                mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork());
+        final NetworkStateSnapshot cellSnapshot = new NetworkStateSnapshot(
+                mCellNetworkAgent.getNetwork(), cellNc, cellLp,
+                null, ConnectivityManager.TYPE_MOBILE);
+        assertEquals(cellSnapshot, snapshots.get(0));
+
+        // Connect wifi and verify the snapshots.
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        waitForIdle();
+        // Compose the expected wifi snapshot for verification.
+        final NetworkCapabilities wifiNc =
+                mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork());
+        final NetworkStateSnapshot wifiSnapshot = new NetworkStateSnapshot(
+                mWiFiNetworkAgent.getNetwork(), wifiNc, new LinkProperties(), null,
+                ConnectivityManager.TYPE_WIFI);
+
+        snapshots = mCm.getAllNetworkStateSnapshots();
+        assertLength(2, snapshots);
+        assertContainsAll(snapshots, cellSnapshot, wifiSnapshot);
+
+        // Set cellular as suspended, verify the snapshots will not contain suspended networks.
+        // TODO: Consider include SUSPENDED networks, which should be considered as
+        //  temporary shortage of connectivity of a connected network.
+        mCellNetworkAgent.suspend();
+        waitForIdle();
+        snapshots = mCm.getAllNetworkStateSnapshots();
+        assertLength(1, snapshots);
+        assertEquals(wifiSnapshot, snapshots.get(0));
+
+        // Disconnect wifi, verify the snapshots contain nothing.
+        mWiFiNetworkAgent.disconnect();
+        waitForIdle();
+        snapshots = mCm.getAllNetworkStateSnapshots();
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertLength(0, snapshots);
+
+        mCellNetworkAgent.resume();
+        waitForIdle();
+        snapshots = mCm.getAllNetworkStateSnapshots();
+        assertLength(1, snapshots);
+        assertEquals(cellSnapshot, snapshots.get(0));
+
+        mCellNetworkAgent.disconnect();
+        waitForIdle();
+        verifyNoNetwork();
+        mCm.unregisterNetworkCallback(cellCb);
+    }
+
+    // Cannot be part of MockNetworkFactory since it requires method of the test.
+    private void expectNoRequestChanged(@NonNull MockNetworkFactory factory) {
+        waitForIdle();
+        factory.assertNoRequestChanged();
+    }
+
+    @Test
+    public void testRegisterBestMatchingNetworkCallback_noIssueToFactory() throws Exception {
+        // Prepare mock mms factory.
+        final HandlerThread handlerThread = new HandlerThread("MockCellularFactory");
+        handlerThread.start();
+        NetworkCapabilities filter = new NetworkCapabilities()
+                .addTransportType(TRANSPORT_CELLULAR)
+                .addCapability(NET_CAPABILITY_MMS);
+        final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "testFactory", filter, mCsHandlerThread);
+        testFactory.setScoreFilter(40);
+
+        try {
+            // Register the factory. It doesn't see the default request because its filter does
+            // not include INTERNET.
+            testFactory.register();
+            expectNoRequestChanged(testFactory);
+            testFactory.assertRequestCountEquals(0);
+            // The factory won't try to start the network since the default request doesn't
+            // match the filter (no INTERNET capability).
+            assertFalse(testFactory.getMyStartRequested());
+
+            // Register callback for listening best matching network. Verify that the request won't
+            // be sent to factory.
+            final TestNetworkCallback bestMatchingCb = new TestNetworkCallback();
+            mCm.registerBestMatchingNetworkCallback(
+                    new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build(),
+                    bestMatchingCb, mCsHandlerThread.getThreadHandler());
+            bestMatchingCb.assertNoCallback();
+            expectNoRequestChanged(testFactory);
+            testFactory.assertRequestCountEquals(0);
+            assertFalse(testFactory.getMyStartRequested());
+
+            // Fire a normal mms request, verify the factory will only see the request.
+            final TestNetworkCallback mmsNetworkCallback = new TestNetworkCallback();
+            final NetworkRequest mmsRequest = new NetworkRequest.Builder()
+                    .addCapability(NET_CAPABILITY_MMS).build();
+            mCm.requestNetwork(mmsRequest, mmsNetworkCallback);
+            testFactory.expectRequestAdd();
+            testFactory.assertRequestCountEquals(1);
+            assertTrue(testFactory.getMyStartRequested());
+
+            // Unregister best matching callback, verify factory see no change.
+            mCm.unregisterNetworkCallback(bestMatchingCb);
+            expectNoRequestChanged(testFactory);
+            testFactory.assertRequestCountEquals(1);
+            assertTrue(testFactory.getMyStartRequested());
+        } finally {
+            testFactory.terminate();
+        }
+    }
+
+    @Test
+    public void testRegisterBestMatchingNetworkCallback_trackBestNetwork() throws Exception {
+        final TestNetworkCallback bestMatchingCb = new TestNetworkCallback();
+        mCm.registerBestMatchingNetworkCallback(
+                new NetworkRequest.Builder().addCapability(NET_CAPABILITY_TRUSTED).build(),
+                bestMatchingCb, mCsHandlerThread.getThreadHandler());
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        bestMatchingCb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        bestMatchingCb.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent);
+
+        // Change something on cellular to trigger capabilities changed, since the callback
+        // only cares about the best network, verify it received nothing from cellular.
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
+        bestMatchingCb.assertNoCallback();
+
+        // Make cellular the best network again, verify the callback now tracks cellular.
+        mWiFiNetworkAgent.adjustScore(-50);
+        bestMatchingCb.expectAvailableCallbacksValidated(mCellNetworkAgent);
+
+        // Make cellular temporary non-trusted, which will not satisfying the request.
+        // Verify the callback switch from/to the other network accordingly.
+        mCellNetworkAgent.removeCapability(NET_CAPABILITY_TRUSTED);
+        bestMatchingCb.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_TRUSTED);
+        bestMatchingCb.expectAvailableDoubleValidatedCallbacks(mCellNetworkAgent);
+
+        // Verify the callback doesn't care about wifi disconnect.
+        mWiFiNetworkAgent.disconnect();
+        bestMatchingCb.assertNoCallback();
+        mCellNetworkAgent.disconnect();
+        bestMatchingCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+    }
+
+    private UidRangeParcel[] uidRangeFor(final UserHandle handle) {
+        UidRange range = UidRange.createForUser(handle);
+        return new UidRangeParcel[] { new UidRangeParcel(range.start, range.stop) };
+    }
+
+    private static class TestOnCompleteListener implements Runnable {
+        final class OnComplete {}
+        final ArrayTrackRecord<OnComplete>.ReadHead mHistory =
+                new ArrayTrackRecord<OnComplete>().newReadHead();
+
+        @Override
+        public void run() {
+            mHistory.add(new OnComplete());
+        }
+
+        public void expectOnComplete() {
+            assertNotNull(mHistory.poll(TIMEOUT_MS, it -> true));
+        }
+    }
+
+    private TestNetworkAgentWrapper makeEnterpriseNetworkAgent() throws Exception {
+        final NetworkCapabilities workNc = new NetworkCapabilities();
+        workNc.addCapability(NET_CAPABILITY_ENTERPRISE);
+        workNc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        return new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), workNc);
+    }
+
+    private TestNetworkCallback mEnterpriseCallback;
+    private UserHandle setupEnterpriseNetwork() {
+        final UserHandle userHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
+        mServiceContext.setWorkProfile(userHandle, true);
+
+        // File a request to avoid the enterprise network being disconnected as soon as the default
+        // request goes away – it would make impossible to test that networkRemoveUidRanges
+        // is called, as the network would disconnect first for lack of a request.
+        mEnterpriseCallback = new TestNetworkCallback();
+        final NetworkRequest keepUpRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_ENTERPRISE)
+                .build();
+        mCm.requestNetwork(keepUpRequest, mEnterpriseCallback);
+        return userHandle;
+    }
+
+    private void maybeTearDownEnterpriseNetwork() {
+        if (null != mEnterpriseCallback) {
+            mCm.unregisterNetworkCallback(mEnterpriseCallback);
+        }
+    }
+
+    /**
+     * Make sure per-profile networking preference behaves as expected when the enterprise network
+     * goes up and down while the preference is active. Make sure they behave as expected whether
+     * there is a general default network or not.
+     */
+    @Test
+    public void testPreferenceForUserNetworkUpDown() throws Exception {
+        final InOrder inOrder = inOrder(mMockNetd);
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        registerDefaultNetworkCallbacks();
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+
+
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+        mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+
+        // Setting a network preference for this user will create a new set of routing rules for
+        // the UID range that corresponds to this user, so as to define the default network
+        // for these apps separately. This is true because the multi-layer request relevant to
+        // this UID range contains a TRACK_DEFAULT, so the range will be moved through UID-specific
+        // rules to the correct network – in this case the system default network. The case where
+        // the default network for the profile happens to be the same as the system default
+        // is not handled specially, the rules are always active as long as a preference is set.
+        inOrder.verify(mMockNetd).networkAddUidRanges(mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle));
+
+        // The enterprise network is not ready yet.
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback,
+                mProfileDefaultNetworkCallback);
+
+        final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent();
+        workAgent.connect(false);
+
+        mProfileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent);
+        mSystemDefaultNetworkCallback.assertNoCallback();
+        mDefaultNetworkCallback.assertNoCallback();
+        inOrder.verify(mMockNetd).networkCreate(
+                nativeNetworkConfigPhysical(workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        inOrder.verify(mMockNetd).networkAddUidRanges(workAgent.getNetwork().netId,
+                uidRangeFor(testHandle));
+        inOrder.verify(mMockNetd).networkRemoveUidRanges(mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle));
+
+        // Make sure changes to the work agent send callbacks to the app in the work profile, but
+        // not to the other apps.
+        workAgent.setNetworkValid(true /* isStrictMode */);
+        workAgent.mNetworkMonitor.forceReevaluation(Process.myUid());
+        mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent,
+                nc -> nc.hasCapability(NET_CAPABILITY_VALIDATED)
+                        && nc.hasCapability(NET_CAPABILITY_ENTERPRISE));
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+
+        workAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
+        mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, nc ->
+                nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+
+        // Conversely, change a capability on the system-wide default network and make sure
+        // that only the apps outside of the work profile receive the callbacks.
+        mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
+        mSystemDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc ->
+                nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+        mDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc ->
+                nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+        mProfileDefaultNetworkCallback.assertNoCallback();
+
+        // Disconnect and reconnect the system-wide default network and make sure that the
+        // apps on this network see the appropriate callbacks, and the app on the work profile
+        // doesn't because it continues to use the enterprise network.
+        mCellNetworkAgent.disconnect();
+        mSystemDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        mProfileDefaultNetworkCallback.assertNoCallback();
+        inOrder.verify(mMockNetd).networkDestroy(mCellNetworkAgent.getNetwork().netId);
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mProfileDefaultNetworkCallback.assertNoCallback();
+        inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+
+        // When the agent disconnects, test that the app on the work profile falls back to the
+        // default network.
+        workAgent.disconnect();
+        mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent);
+        mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        inOrder.verify(mMockNetd).networkAddUidRanges(mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle));
+        inOrder.verify(mMockNetd).networkDestroy(workAgent.getNetwork().netId);
+
+        mCellNetworkAgent.disconnect();
+        mSystemDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+
+        // Waiting for the handler to be idle before checking for networkDestroy is necessary
+        // here because ConnectivityService calls onLost before the network is fully torn down.
+        waitForIdle();
+        inOrder.verify(mMockNetd).networkDestroy(mCellNetworkAgent.getNetwork().netId);
+
+        // If the control comes here, callbacks seem to behave correctly in the presence of
+        // a default network when the enterprise network goes up and down. Now, make sure they
+        // also behave correctly in the absence of a system-wide default network.
+        final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent();
+        workAgent2.connect(false);
+
+        mProfileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent2);
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+        inOrder.verify(mMockNetd).networkAddUidRanges(workAgent2.getNetwork().netId,
+                uidRangeFor(testHandle));
+
+        workAgent2.setNetworkValid(true /* isStrictMode */);
+        workAgent2.mNetworkMonitor.forceReevaluation(Process.myUid());
+        mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent2,
+                nc -> nc.hasCapability(NET_CAPABILITY_ENTERPRISE)
+                        && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        inOrder.verify(mMockNetd, never()).networkAddUidRanges(anyInt(), any());
+
+        // When the agent disconnects, test that the app on the work profile falls back to the
+        // default network.
+        workAgent2.disconnect();
+        mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent2);
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        inOrder.verify(mMockNetd).networkDestroy(workAgent2.getNetwork().netId);
+
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback,
+                mProfileDefaultNetworkCallback);
+
+        // 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.
+     */
+    @Test
+    public void testSetPreferenceForUserOnOff() throws Exception {
+        final InOrder inOrder = inOrder(mMockNetd);
+        final UserHandle testHandle = setupEnterpriseNetwork();
+
+        // Connect both a regular cell agent and an enterprise network first.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent();
+        workAgent.connect(true);
+
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+        mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+        inOrder.verify(mMockNetd).networkAddUidRanges(workAgent.getNetwork().netId,
+                uidRangeFor(testHandle));
+
+        registerDefaultNetworkCallbacks();
+
+        mSystemDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(workAgent);
+
+        mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_DEFAULT,
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+
+        mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+        inOrder.verify(mMockNetd).networkRemoveUidRanges(workAgent.getNetwork().netId,
+                uidRangeFor(testHandle));
+
+        workAgent.disconnect();
+        mCellNetworkAgent.disconnect();
+
+        // Callbacks will be unregistered by tearDown()
+    }
+
+    /**
+     * Test per-profile default networks for two different profiles concurrently.
+     */
+    @Test
+    public void testSetPreferenceForTwoProfiles() throws Exception {
+        final InOrder inOrder = inOrder(mMockNetd);
+        final UserHandle testHandle2 = setupEnterpriseNetwork();
+        final UserHandle testHandle4 = UserHandle.of(TEST_WORK_PROFILE_USER_ID + 2);
+        mServiceContext.setWorkProfile(testHandle4, true);
+        registerDefaultNetworkCallbacks();
+
+        final TestNetworkCallback app4Cb = new TestNetworkCallback();
+        final int testWorkProfileAppUid4 =
+                UserHandle.getUid(testHandle4.getIdentifier(), TEST_APP_ID);
+        registerDefaultNetworkCallbackAsUid(app4Cb, testWorkProfileAppUid4);
+
+        // Connect both a regular cell agent and an enterprise network first.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent();
+        workAgent.connect(true);
+
+        mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        app4Cb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+        inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+        mCm.setProfileNetworkPreference(testHandle2, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        inOrder.verify(mMockNetd).networkAddUidRanges(workAgent.getNetwork().netId,
+                uidRangeFor(testHandle2));
+
+        mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(workAgent);
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback,
+                app4Cb);
+
+        mCm.setProfileNetworkPreference(testHandle4, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        inOrder.verify(mMockNetd).networkAddUidRanges(workAgent.getNetwork().netId,
+                uidRangeFor(testHandle4));
+
+        app4Cb.expectAvailableCallbacksValidated(workAgent);
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback,
+                mProfileDefaultNetworkCallback);
+
+        mCm.setProfileNetworkPreference(testHandle2, PROFILE_NETWORK_PREFERENCE_DEFAULT,
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        inOrder.verify(mMockNetd).networkRemoveUidRanges(workAgent.getNetwork().netId,
+                uidRangeFor(testHandle2));
+
+        mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback,
+                app4Cb);
+
+        workAgent.disconnect();
+        mCellNetworkAgent.disconnect();
+
+        mCm.unregisterNetworkCallback(app4Cb);
+        // Other callbacks will be unregistered by tearDown()
+    }
+
+    @Test
+    public void testProfilePreferenceRemovedUponUserRemoved() throws Exception {
+        final InOrder inOrder = inOrder(mMockNetd);
+        final UserHandle testHandle = setupEnterpriseNetwork();
+
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+        mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+                mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+        inOrder.verify(mMockNetd).networkAddUidRanges(mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle));
+
+        final Intent removedIntent = new Intent(ACTION_USER_REMOVED);
+        removedIntent.putExtra(Intent.EXTRA_USER, testHandle);
+        processBroadcast(removedIntent);
+
+        inOrder.verify(mMockNetd).networkRemoveUidRanges(mCellNetworkAgent.getNetwork().netId,
+                uidRangeFor(testHandle));
+    }
+
+    /**
+     * Make sure that OEM preference and per-profile preference can't be used at the same
+     * time and throw ISE if tried
+     */
+    @Test
+    public void testOemPreferenceAndProfilePreferenceExclusive() throws Exception {
+        final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
+        mServiceContext.setWorkProfile(testHandle, true);
+        final TestOnCompleteListener listener = new TestOnCompleteListener();
+
+        setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest(
+                OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY);
+        assertThrows("Should not be able to set per-profile pref while OEM prefs present",
+                IllegalStateException.class, () ->
+                        mCm.setProfileNetworkPreference(testHandle,
+                                PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+                                r -> r.run(), listener));
+
+        // Empty the OEM prefs
+        final TestOemListenerCallback oemPrefListener = new TestOemListenerCallback();
+        final OemNetworkPreferences emptyOemPref = new OemNetworkPreferences.Builder().build();
+        mService.setOemNetworkPreference(emptyOemPref, oemPrefListener);
+        oemPrefListener.expectOnComplete();
+
+        mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+                r -> r.run(), listener);
+        listener.expectOnComplete();
+        assertThrows("Should not be able to set OEM prefs while per-profile pref is on",
+                IllegalStateException.class , () ->
+                        mService.setOemNetworkPreference(emptyOemPref, oemPrefListener));
+    }
+
+    /**
+     * Make sure wrong preferences for per-profile default networking are rejected.
+     */
+    @Test
+    public void testProfileNetworkPrefWrongPreference() throws Exception {
+        final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
+        mServiceContext.setWorkProfile(testHandle, true);
+        assertThrows("Should not be able to set an illegal preference",
+                IllegalArgumentException.class,
+                () -> mCm.setProfileNetworkPreference(testHandle,
+                        PROFILE_NETWORK_PREFERENCE_ENTERPRISE + 1, null, null));
+    }
+
+    /**
+     * Make sure requests for per-profile default networking for a non-work profile are
+     * rejected
+     */
+    @Test
+    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",
+                IllegalArgumentException.class , () ->
+                        mCm.setProfileNetworkPreference(testHandle,
+                                PROFILE_NETWORK_PREFERENCE_ENTERPRISE, null, null));
+    }
+
+    @Test
+    public void testSubIdsClearedWithoutNetworkFactoryPermission() throws Exception {
+        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setSubscriptionIds(Collections.singleton(Process.myUid()));
+
+        final NetworkCapabilities result =
+                mService.networkCapabilitiesRestrictedForCallerPermissions(
+                        nc, Process.myPid(), Process.myUid());
+        assertTrue(result.getSubscriptionIds().isEmpty());
+    }
+
+    @Test
+    public void testSubIdsExistWithNetworkFactoryPermission() throws Exception {
+        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
+
+        final Set<Integer> subIds = Collections.singleton(Process.myUid());
+        final NetworkCapabilities nc = new NetworkCapabilities();
+        nc.setSubscriptionIds(subIds);
+
+        final NetworkCapabilities result =
+                mService.networkCapabilitiesRestrictedForCallerPermissions(
+                        nc, Process.myPid(), Process.myUid());
+        assertEquals(subIds, result.getSubscriptionIds());
+    }
+
+    private NetworkRequest getRequestWithSubIds() {
+        return new NetworkRequest.Builder()
+                .setSubscriptionIds(Collections.singleton(Process.myUid()))
+                .build();
+    }
+
+    @Test
+    public void testNetworkRequestWithSubIdsWithNetworkFactoryPermission() throws Exception {
+        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
+        final PendingIntent pendingIntent = PendingIntent.getBroadcast(
+                mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE);
+        final NetworkCallback networkCallback1 = new NetworkCallback();
+        final NetworkCallback networkCallback2 = new NetworkCallback();
+
+        mCm.requestNetwork(getRequestWithSubIds(), networkCallback1);
+        mCm.requestNetwork(getRequestWithSubIds(), pendingIntent);
+        mCm.registerNetworkCallback(getRequestWithSubIds(), networkCallback2);
+
+        mCm.unregisterNetworkCallback(networkCallback1);
+        mCm.releaseNetworkRequest(pendingIntent);
+        mCm.unregisterNetworkCallback(networkCallback2);
+    }
+
+    @Test
+    public void testNetworkRequestWithSubIdsWithoutNetworkFactoryPermission() throws Exception {
+        mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
+        final PendingIntent pendingIntent = PendingIntent.getBroadcast(
+                mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE);
+
+        final Class<SecurityException> expected = SecurityException.class;
+        assertThrows(
+                expected, () -> mCm.requestNetwork(getRequestWithSubIds(), new NetworkCallback()));
+        assertThrows(expected, () -> mCm.requestNetwork(getRequestWithSubIds(), pendingIntent));
+        assertThrows(
+                expected,
+                () -> mCm.registerNetworkCallback(getRequestWithSubIds(), new NetworkCallback()));
+    }
+
+    /**
+     * Validate request counts are counted accurately on setProfileNetworkPreference on set/replace.
+     */
+    @Test
+    public void testProfileNetworkPrefCountsRequestsCorrectlyOnSet() throws Exception {
+        final UserHandle testHandle = setupEnterpriseNetwork();
+        testRequestCountLimits(() -> {
+            // Set initially to test the limit prior to having existing requests.
+            final TestOnCompleteListener listener = new TestOnCompleteListener();
+            mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+                    Runnable::run, listener);
+            listener.expectOnComplete();
+
+            // re-set so as to test the limit as part of replacing existing requests.
+            mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+                    Runnable::run, listener);
+            listener.expectOnComplete();
+        });
+    }
+
+    /**
+     * Validate request counts are counted accurately on setOemNetworkPreference on set/replace.
+     */
+    @Test
+    public void testSetOemNetworkPreferenceCountsRequestsCorrectlyOnSet() throws Exception {
+        mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true);
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY;
+        testRequestCountLimits(() -> {
+            // Set initially to test the limit prior to having existing requests.
+            final TestOemListenerCallback listener = new TestOemListenerCallback();
+            mService.setOemNetworkPreference(
+                    createDefaultOemNetworkPreferences(networkPref), listener);
+            listener.expectOnComplete();
+
+            // re-set so as to test the limit as part of replacing existing requests.
+            mService.setOemNetworkPreference(
+                    createDefaultOemNetworkPreferences(networkPref), listener);
+            listener.expectOnComplete();
+        });
+    }
+
+    private void testRequestCountLimits(@NonNull final Runnable r) throws Exception {
+        final ArraySet<TestNetworkCallback> callbacks = new ArraySet<>();
+        try {
+            final int requestCount = mService.mSystemNetworkRequestCounter
+                    .mUidToNetworkRequestCount.get(Process.myUid());
+            // The limit is hit when total requests <= limit.
+            final int maxCount =
+                    ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - requestCount;
+            // Need permission so registerDefaultNetworkCallback uses mSystemNetworkRequestCounter
+            withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, () -> {
+                for (int i = 1; i < maxCount - 1; i++) {
+                    final TestNetworkCallback cb = new TestNetworkCallback();
+                    mCm.registerDefaultNetworkCallback(cb);
+                    callbacks.add(cb);
+                }
+
+                // Code to run to check if it triggers a max request count limit error.
+                r.run();
+            });
+        } finally {
+            for (final TestNetworkCallback cb : callbacks) {
+                mCm.unregisterNetworkCallback(cb);
+            }
+        }
+    }
+
+    private void assertCreateNrisFromMobileDataPreferredUids(Set<Integer> uids) {
+        final Set<NetworkRequestInfo> nris =
+                mService.createNrisFromMobileDataPreferredUids(uids);
+        final NetworkRequestInfo nri = nris.iterator().next();
+        // Verify that one NRI is created with multilayer requests. Because one NRI can contain
+        // multiple uid ranges, so it only need create one NRI here.
+        assertEquals(1, nris.size());
+        assertTrue(nri.isMultilayerRequest());
+        assertEquals(nri.getUids(), uidRangesForUids(uids));
+        assertEquals(DEFAULT_NETWORK_PRIORITY_MOBILE_DATA_PREFERRED,
+                nri.getDefaultNetworkPriority());
+    }
+
+    /**
+     * Test createNrisFromMobileDataPreferredUids returns correct NetworkRequestInfo.
+     */
+    @Test
+    public void testCreateNrisFromMobileDataPreferredUids() {
+        // Verify that empty uid set should not create any NRI for it.
+        final Set<NetworkRequestInfo> nrisNoUid =
+                mService.createNrisFromMobileDataPreferredUids(new ArraySet<>());
+        assertEquals(0, nrisNoUid.size());
+
+        final int uid1 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID);
+        final int uid2 = PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID2);
+        final int uid3 = SECONDARY_USER_HANDLE.getUid(TEST_PACKAGE_UID);
+        assertCreateNrisFromMobileDataPreferredUids(Set.of(uid1));
+        assertCreateNrisFromMobileDataPreferredUids(Set.of(uid1, uid3));
+        assertCreateNrisFromMobileDataPreferredUids(Set.of(uid1, uid2));
+    }
+
+    private void setAndUpdateMobileDataPreferredUids(Set<Integer> uids) {
+        ConnectivitySettingsManager.setMobileDataPreferredUids(mServiceContext, uids);
+        mService.updateMobileDataPreferredUids();
+        waitForIdle();
+    }
+
+    /**
+     * Test that MOBILE_DATA_PREFERRED_UIDS changes will send correct net id and uid ranges to netd.
+     */
+    @Test
+    public void testMobileDataPreferredUidsChanged() throws Exception {
+        final InOrder inorder = inOrder(mMockNetd);
+        registerDefaultNetworkCallbacks();
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mTestPackageDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        final int cellNetId = mCellNetworkAgent.getNetwork().netId;
+        inorder.verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(
+                cellNetId, INetd.PERMISSION_NONE));
+
+        // Initial mobile data preferred uids status.
+        setAndUpdateMobileDataPreferredUids(Set.of());
+        inorder.verify(mMockNetd, never()).networkAddUidRanges(anyInt(), any());
+        inorder.verify(mMockNetd, never()).networkRemoveUidRanges(anyInt(), any());
+
+        // Set MOBILE_DATA_PREFERRED_UIDS setting and verify that net id and uid ranges send to netd
+        final Set<Integer> uids1 = Set.of(PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID));
+        final UidRangeParcel[] uidRanges1 = toUidRangeStableParcels(uidRangesForUids(uids1));
+        setAndUpdateMobileDataPreferredUids(uids1);
+        inorder.verify(mMockNetd, times(1)).networkAddUidRanges(cellNetId, uidRanges1);
+        inorder.verify(mMockNetd, never()).networkRemoveUidRanges(anyInt(), any());
+
+        // Set MOBILE_DATA_PREFERRED_UIDS setting again and verify that old rules are removed and
+        // new rules are added.
+        final Set<Integer> uids2 = Set.of(PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID),
+                PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID2),
+                SECONDARY_USER_HANDLE.getUid(TEST_PACKAGE_UID));
+        final UidRangeParcel[] uidRanges2 = toUidRangeStableParcels(uidRangesForUids(uids2));
+        setAndUpdateMobileDataPreferredUids(uids2);
+        inorder.verify(mMockNetd, times(1)).networkRemoveUidRanges(cellNetId, uidRanges1);
+        inorder.verify(mMockNetd, times(1)).networkAddUidRanges(cellNetId, uidRanges2);
+
+        // Clear MOBILE_DATA_PREFERRED_UIDS setting again and verify that old rules are removed and
+        // new rules are not added.
+        final Set<Integer> uids3 = Set.of();
+        final UidRangeParcel[] uidRanges3 = toUidRangeStableParcels(uidRangesForUids(uids3));
+        setAndUpdateMobileDataPreferredUids(uids3);
+        inorder.verify(mMockNetd, times(1)).networkRemoveUidRanges(cellNetId, uidRanges2);
+        inorder.verify(mMockNetd, never()).networkAddUidRanges(anyInt(), any());
+    }
+
+    /**
+     * Make sure mobile data preferred uids feature behaves as expected when the mobile network
+     * goes up and down while the uids is set. Make sure they behave as expected whether
+     * there is a general default network or not.
+     */
+    @Test
+    public void testMobileDataPreferenceForMobileNetworkUpDown() throws Exception {
+        final InOrder inorder = inOrder(mMockNetd);
+        // File a request for cell to ensure it doesn't go down.
+        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+        final NetworkRequest cellRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_CELLULAR).build();
+        mCm.requestNetwork(cellRequest, cellNetworkCallback);
+        cellNetworkCallback.assertNoCallback();
+
+        registerDefaultNetworkCallbacks();
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+        mTestPackageDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(TEST_PACKAGE_UID));
+
+        final int wifiNetId = mWiFiNetworkAgent.getNetwork().netId;
+        inorder.verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(
+                wifiNetId, INetd.PERMISSION_NONE));
+
+        // Initial mobile data preferred uids status.
+        setAndUpdateMobileDataPreferredUids(Set.of());
+        inorder.verify(mMockNetd, never()).networkAddUidRanges(anyInt(), any());
+        inorder.verify(mMockNetd, never()).networkRemoveUidRanges(anyInt(), any());
+
+        // Set MOBILE_DATA_PREFERRED_UIDS setting and verify that wifi net id and uid ranges send to
+        // netd.
+        final Set<Integer> uids = Set.of(PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID));
+        final UidRangeParcel[] uidRanges = toUidRangeStableParcels(uidRangesForUids(uids));
+        setAndUpdateMobileDataPreferredUids(uids);
+        inorder.verify(mMockNetd, times(1)).networkAddUidRanges(wifiNetId, uidRanges);
+        inorder.verify(mMockNetd, never()).networkRemoveUidRanges(anyInt(), any());
+
+        // Cellular network connected. mTestPackageDefaultNetworkCallback should receive
+        // callback with cellular network and net id and uid ranges should be updated to netd.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.assertNoCallback();
+        mTestPackageDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(TEST_PACKAGE_UID));
+
+        final int cellNetId = mCellNetworkAgent.getNetwork().netId;
+        inorder.verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(
+                cellNetId, INetd.PERMISSION_NONE));
+        inorder.verify(mMockNetd, times(1)).networkAddUidRanges(cellNetId, uidRanges);
+        inorder.verify(mMockNetd, times(1)).networkRemoveUidRanges(wifiNetId, uidRanges);
+
+        // Cellular network disconnected. mTestPackageDefaultNetworkCallback should receive
+        // callback with wifi network from fallback request.
+        mCellNetworkAgent.disconnect();
+        mDefaultNetworkCallback.assertNoCallback();
+        cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        mTestPackageDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+        mTestPackageDefaultNetworkCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(TEST_PACKAGE_UID));
+        inorder.verify(mMockNetd, times(1)).networkAddUidRanges(wifiNetId, uidRanges);
+        inorder.verify(mMockNetd, never()).networkRemoveUidRanges(anyInt(), any());
+        inorder.verify(mMockNetd).networkDestroy(cellNetId);
+
+        // Cellular network comes back. mTestPackageDefaultNetworkCallback should receive
+        // callback with cellular network.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        mDefaultNetworkCallback.assertNoCallback();
+        mTestPackageDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(TEST_PACKAGE_UID));
+
+        final int cellNetId2 = mCellNetworkAgent.getNetwork().netId;
+        inorder.verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(
+                cellNetId2, INetd.PERMISSION_NONE));
+        inorder.verify(mMockNetd, times(1)).networkAddUidRanges(cellNetId2, uidRanges);
+        inorder.verify(mMockNetd, times(1)).networkRemoveUidRanges(wifiNetId, uidRanges);
+
+        // Wifi network disconnected. mTestPackageDefaultNetworkCallback should not receive
+        // any callback.
+        mWiFiNetworkAgent.disconnect();
+        mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        mDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+        mTestPackageDefaultNetworkCallback.assertNoCallback();
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(TEST_PACKAGE_UID));
+        waitForIdle();
+        inorder.verify(mMockNetd, never()).networkAddUidRanges(anyInt(), any());
+        inorder.verify(mMockNetd, never()).networkRemoveUidRanges(anyInt(), any());
+        inorder.verify(mMockNetd).networkDestroy(wifiNetId);
+
+        mCm.unregisterNetworkCallback(cellNetworkCallback);
+    }
+
+    @Test
+    public void testSetMobileDataPreferredUids_noIssueToFactory() throws Exception {
+        // First set mobile data preferred uid to create a multi-layer requests: 1. listen for
+        // cellular, 2. track the default network for fallback.
+        setAndUpdateMobileDataPreferredUids(
+                Set.of(PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)));
+
+        final HandlerThread handlerThread = new HandlerThread("MockFactory");
+        handlerThread.start();
+        NetworkCapabilities internetFilter = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+        final MockNetworkFactory internetFactory = new MockNetworkFactory(handlerThread.getLooper(),
+                mServiceContext, "internetFactory", internetFilter, mCsHandlerThread);
+        internetFactory.setScoreFilter(40);
+
+        try {
+            internetFactory.register();
+            // Default internet request only. The first request is listen for cellular network,
+            // which is never sent to factories (it's a LISTEN, not requestable). The second
+            // fallback request is TRACK_DEFAULT which is also not sent to factories.
+            internetFactory.expectRequestAdds(1);
+            internetFactory.assertRequestCountEquals(1);
+
+            mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+            mCellNetworkAgent.connect(true);
+
+            // The internet factory however is outscored, and should lose its requests.
+            internetFactory.expectRequestRemove();
+            internetFactory.assertRequestCountEquals(0);
+
+            mCellNetworkAgent.disconnect();
+            // The network satisfying the default internet request has disconnected, so the
+            // internetFactory sees the default request again.
+            internetFactory.expectRequestAdds(1);
+            internetFactory.assertRequestCountEquals(1);
+        } finally {
+            internetFactory.terminate();
+            handlerThread.quitSafely();
+        }
+    }
+
+    /**
+     * Validate request counts are counted accurately on MOBILE_DATA_PREFERRED_UIDS change
+     * on set/replace.
+     */
+    @Test
+    public void testMobileDataPreferredUidsChangedCountsRequestsCorrectlyOnSet() throws Exception {
+        ConnectivitySettingsManager.setMobileDataPreferredUids(mServiceContext,
+                Set.of(PRIMARY_USER_HANDLE.getUid(TEST_PACKAGE_UID)));
+        testRequestCountLimits(() -> {
+            // Set initially to test the limit prior to having existing requests.
+            mService.updateMobileDataPreferredUids();
+            waitForIdle();
+
+            // re-set so as to test the limit as part of replacing existing requests.
+            mService.updateMobileDataPreferredUids();
+            waitForIdle();
+        });
+    }
+}
diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
new file mode 100644
index 0000000..cf2c9c7
--- /dev/null
+++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
@@ -0,0 +1,1004 @@
+/*
+ * 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.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.INetd.IF_STATE_DOWN;
+import static android.net.INetd.IF_STATE_UP;
+import static android.net.IpSecManager.DIRECTION_FWD;
+import static android.net.IpSecManager.DIRECTION_IN;
+import static android.net.IpSecManager.DIRECTION_OUT;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
+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.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.system.Os;
+import android.test.mock.MockContext;
+import android.util.ArraySet;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.IpSecService.TunnelInterfaceRecord;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.net.Inet4Address;
+import java.net.Socket;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Set;
+
+/** Unit tests for {@link IpSecService}. */
+@SmallTest
+@RunWith(Parameterized.class)
+public class IpSecServiceParameterizedTest {
+
+    private static final int TEST_SPI = 0xD1201D;
+
+    private final String mSourceAddr;
+    private final String mDestinationAddr;
+    private final LinkAddress mLocalInnerAddress;
+    private final int mFamily;
+
+    private static final int[] ADDRESS_FAMILIES =
+            new int[] {AF_INET, AF_INET6};
+
+    @Parameterized.Parameters
+    public static Collection ipSecConfigs() {
+        return Arrays.asList(
+                new Object[][] {
+                {"1.2.3.4", "8.8.4.4", "10.0.1.1/24", AF_INET},
+                {"2601::2", "2601::10", "2001:db8::1/64", AF_INET6}
+        });
+    }
+
+    private static final byte[] AEAD_KEY = {
+        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+        0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+        0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+        0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
+        0x73, 0x61, 0x6C, 0x74
+    };
+    private static final byte[] CRYPT_KEY = {
+        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+        0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+        0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+        0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F
+    };
+    private static final byte[] AUTH_KEY = {
+        0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F,
+        0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F
+    };
+
+    AppOpsManager mMockAppOps = mock(AppOpsManager.class);
+    ConnectivityManager mMockConnectivityMgr = mock(ConnectivityManager.class);
+
+    TestContext mTestContext = new TestContext();
+
+    private class TestContext extends MockContext {
+        private Set<String> mAllowedPermissions = new ArraySet<>(Arrays.asList(
+                android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
+                android.Manifest.permission.NETWORK_STACK,
+                PERMISSION_MAINLINE_NETWORK_STACK));
+
+        private void setAllowedPermissions(String... permissions) {
+            mAllowedPermissions = new ArraySet<>(permissions);
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            switch(name) {
+                case Context.APP_OPS_SERVICE:
+                    return mMockAppOps;
+                case Context.CONNECTIVITY_SERVICE:
+                    return mMockConnectivityMgr;
+                default:
+                    return null;
+            }
+        }
+
+        @Override
+        public String getSystemServiceName(Class<?> serviceClass) {
+            if (ConnectivityManager.class == serviceClass) {
+                return Context.CONNECTIVITY_SERVICE;
+            }
+            return null;
+        }
+
+        @Override
+        public PackageManager getPackageManager() {
+            return mMockPkgMgr;
+        }
+
+        @Override
+        public void enforceCallingOrSelfPermission(String permission, String message) {
+            if (mAllowedPermissions.contains(permission)) {
+                return;
+            } else {
+                throw new SecurityException("Unavailable permission requested");
+            }
+        }
+
+        @Override
+        public int checkCallingOrSelfPermission(String permission) {
+            if (mAllowedPermissions.contains(permission)) {
+                return PERMISSION_GRANTED;
+            } else {
+                return PERMISSION_DENIED;
+            }
+        }
+    }
+
+    INetd mMockNetd;
+    PackageManager mMockPkgMgr;
+    IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig;
+    IpSecService mIpSecService;
+    Network fakeNetwork = new Network(0xAB);
+    int mUid = Os.getuid();
+
+    private static final IpSecAlgorithm AUTH_ALGO =
+            new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4);
+    private static final IpSecAlgorithm CRYPT_ALGO =
+            new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+    private static final IpSecAlgorithm AEAD_ALGO =
+            new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+    private static final int REMOTE_ENCAP_PORT = 4500;
+
+    private static final String BLESSED_PACKAGE = "blessedPackage";
+    private static final String SYSTEM_PACKAGE = "systemPackage";
+    private static final String BAD_PACKAGE = "badPackage";
+
+    public IpSecServiceParameterizedTest(
+            String sourceAddr, String destAddr, String localInnerAddr, int family) {
+        mSourceAddr = sourceAddr;
+        mDestinationAddr = destAddr;
+        mLocalInnerAddress = new LinkAddress(localInnerAddr);
+        mFamily = family;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mMockNetd = mock(INetd.class);
+        mMockPkgMgr = mock(PackageManager.class);
+        mMockIpSecSrvConfig = mock(IpSecService.IpSecServiceConfiguration.class);
+        mIpSecService = new IpSecService(mTestContext, mMockIpSecSrvConfig);
+
+        // Injecting mock netd
+        when(mMockIpSecSrvConfig.getNetdInstance()).thenReturn(mMockNetd);
+
+        // PackageManager should always return true (feature flag tests in IpSecServiceTest)
+        when(mMockPkgMgr.hasSystemFeature(anyString())).thenReturn(true);
+
+        // A package granted the AppOp for MANAGE_IPSEC_TUNNELS will be MODE_ALLOWED.
+        when(mMockAppOps.noteOp(anyInt(), anyInt(), eq(BLESSED_PACKAGE)))
+                .thenReturn(AppOpsManager.MODE_ALLOWED);
+        // A system package will not be granted the app op, so this should fall back to
+        // a permissions check, which should pass.
+        when(mMockAppOps.noteOp(anyInt(), anyInt(), eq(SYSTEM_PACKAGE)))
+                .thenReturn(AppOpsManager.MODE_DEFAULT);
+        // A mismatch between the package name and the UID will return MODE_IGNORED.
+        when(mMockAppOps.noteOp(anyInt(), anyInt(), eq(BAD_PACKAGE)))
+                .thenReturn(AppOpsManager.MODE_IGNORED);
+    }
+
+    //TODO: Add a test to verify SPI.
+
+    @Test
+    public void testIpSecServiceReserveSpi() throws Exception {
+        when(mMockNetd.ipSecAllocateSpi(anyInt(), anyString(), eq(mDestinationAddr), eq(TEST_SPI)))
+                .thenReturn(TEST_SPI);
+
+        IpSecSpiResponse spiResp =
+                mIpSecService.allocateSecurityParameterIndex(
+                        mDestinationAddr, TEST_SPI, new Binder());
+        assertEquals(IpSecManager.Status.OK, spiResp.status);
+        assertEquals(TEST_SPI, spiResp.spi);
+    }
+
+    @Test
+    public void testReleaseSecurityParameterIndex() throws Exception {
+        when(mMockNetd.ipSecAllocateSpi(anyInt(), anyString(), eq(mDestinationAddr), eq(TEST_SPI)))
+                .thenReturn(TEST_SPI);
+
+        IpSecSpiResponse spiResp =
+                mIpSecService.allocateSecurityParameterIndex(
+                        mDestinationAddr, TEST_SPI, new Binder());
+
+        mIpSecService.releaseSecurityParameterIndex(spiResp.resourceId);
+
+        verify(mMockNetd)
+                .ipSecDeleteSecurityAssociation(
+                        eq(mUid),
+                        anyString(),
+                        anyString(),
+                        eq(TEST_SPI),
+                        anyInt(),
+                        anyInt(),
+                        anyInt());
+
+        // Verify quota and RefcountedResource objects cleaned up
+        IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        assertEquals(0, userRecord.mSpiQuotaTracker.mCurrent);
+        try {
+            userRecord.mSpiRecords.getRefcountedResourceOrThrow(spiResp.resourceId);
+            fail("Expected IllegalArgumentException on attempt to access deleted resource");
+        } catch (IllegalArgumentException expected) {
+
+        }
+    }
+
+    @Test
+    public void testSecurityParameterIndexBinderDeath() throws Exception {
+        when(mMockNetd.ipSecAllocateSpi(anyInt(), anyString(), eq(mDestinationAddr), eq(TEST_SPI)))
+                .thenReturn(TEST_SPI);
+
+        IpSecSpiResponse spiResp =
+                mIpSecService.allocateSecurityParameterIndex(
+                        mDestinationAddr, TEST_SPI, new Binder());
+
+        IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        IpSecService.RefcountedResource refcountedRecord =
+                userRecord.mSpiRecords.getRefcountedResourceOrThrow(spiResp.resourceId);
+
+        refcountedRecord.binderDied();
+
+        verify(mMockNetd)
+                .ipSecDeleteSecurityAssociation(
+                        eq(mUid),
+                        anyString(),
+                        anyString(),
+                        eq(TEST_SPI),
+                        anyInt(),
+                        anyInt(),
+                        anyInt());
+
+        // Verify quota and RefcountedResource objects cleaned up
+        assertEquals(0, userRecord.mSpiQuotaTracker.mCurrent);
+        try {
+            userRecord.mSpiRecords.getRefcountedResourceOrThrow(spiResp.resourceId);
+            fail("Expected IllegalArgumentException on attempt to access deleted resource");
+        } catch (IllegalArgumentException expected) {
+
+        }
+    }
+
+    private int getNewSpiResourceId(String remoteAddress, int returnSpi) throws Exception {
+        when(mMockNetd.ipSecAllocateSpi(anyInt(), anyString(), anyString(), anyInt()))
+                .thenReturn(returnSpi);
+
+        IpSecSpiResponse spi =
+                mIpSecService.allocateSecurityParameterIndex(
+                        InetAddresses.parseNumericAddress(remoteAddress).getHostAddress(),
+                        IpSecManager.INVALID_SECURITY_PARAMETER_INDEX,
+                        new Binder());
+        return spi.resourceId;
+    }
+
+    private void addDefaultSpisAndRemoteAddrToIpSecConfig(IpSecConfig config) throws Exception {
+        config.setSpiResourceId(getNewSpiResourceId(mDestinationAddr, TEST_SPI));
+        config.setSourceAddress(mSourceAddr);
+        config.setDestinationAddress(mDestinationAddr);
+    }
+
+    private void addAuthAndCryptToIpSecConfig(IpSecConfig config) throws Exception {
+        config.setEncryption(CRYPT_ALGO);
+        config.setAuthentication(AUTH_ALGO);
+    }
+
+    private void addEncapSocketToIpSecConfig(int resourceId, IpSecConfig config) throws Exception {
+        config.setEncapType(IpSecTransform.ENCAP_ESPINUDP);
+        config.setEncapSocketResourceId(resourceId);
+        config.setEncapRemotePort(REMOTE_ENCAP_PORT);
+    }
+
+    private void verifyTransformNetdCalledForCreatingSA(
+            IpSecConfig config, IpSecTransformResponse resp) throws Exception {
+        verifyTransformNetdCalledForCreatingSA(config, resp, 0);
+    }
+
+    private void verifyTransformNetdCalledForCreatingSA(
+            IpSecConfig config, IpSecTransformResponse resp, int encapSocketPort) throws Exception {
+        IpSecAlgorithm auth = config.getAuthentication();
+        IpSecAlgorithm crypt = config.getEncryption();
+        IpSecAlgorithm authCrypt = config.getAuthenticatedEncryption();
+
+        verify(mMockNetd, times(1))
+                .ipSecAddSecurityAssociation(
+                        eq(mUid),
+                        eq(config.getMode()),
+                        eq(config.getSourceAddress()),
+                        eq(config.getDestinationAddress()),
+                        eq((config.getNetwork() != null) ? config.getNetwork().netId : 0),
+                        eq(TEST_SPI),
+                        eq(0),
+                        eq(0),
+                        eq((auth != null) ? auth.getName() : ""),
+                        eq((auth != null) ? auth.getKey() : new byte[] {}),
+                        eq((auth != null) ? auth.getTruncationLengthBits() : 0),
+                        eq((crypt != null) ? crypt.getName() : ""),
+                        eq((crypt != null) ? crypt.getKey() : new byte[] {}),
+                        eq((crypt != null) ? crypt.getTruncationLengthBits() : 0),
+                        eq((authCrypt != null) ? authCrypt.getName() : ""),
+                        eq((authCrypt != null) ? authCrypt.getKey() : new byte[] {}),
+                        eq((authCrypt != null) ? authCrypt.getTruncationLengthBits() : 0),
+                        eq(config.getEncapType()),
+                        eq(encapSocketPort),
+                        eq(config.getEncapRemotePort()),
+                        eq(config.getXfrmInterfaceId()));
+    }
+
+    @Test
+    public void testCreateTransform() throws Exception {
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+        assertEquals(IpSecManager.Status.OK, createTransformResp.status);
+
+        verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp);
+    }
+
+    @Test
+    public void testCreateTransformAead() throws Exception {
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+
+        ipSecConfig.setAuthenticatedEncryption(AEAD_ALGO);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+        assertEquals(IpSecManager.Status.OK, createTransformResp.status);
+
+        verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp);
+    }
+
+    @Test
+    public void testCreateTransportModeTransformWithEncap() throws Exception {
+        IpSecUdpEncapResponse udpSock = mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        ipSecConfig.setMode(IpSecTransform.MODE_TRANSPORT);
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+        addEncapSocketToIpSecConfig(udpSock.resourceId, ipSecConfig);
+
+        if (mFamily == AF_INET) {
+            IpSecTransformResponse createTransformResp =
+                    mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+            assertEquals(IpSecManager.Status.OK, createTransformResp.status);
+
+            verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp, udpSock.port);
+        } else {
+            try {
+                IpSecTransformResponse createTransformResp =
+                        mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+                fail("Expected IllegalArgumentException on attempt to use UDP Encap in IPv6");
+            } catch (IllegalArgumentException expected) {
+            }
+        }
+    }
+
+    @Test
+    public void testCreateTunnelModeTransformWithEncap() throws Exception {
+        IpSecUdpEncapResponse udpSock = mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        ipSecConfig.setMode(IpSecTransform.MODE_TUNNEL);
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+        addEncapSocketToIpSecConfig(udpSock.resourceId, ipSecConfig);
+
+        if (mFamily == AF_INET) {
+            IpSecTransformResponse createTransformResp =
+                    mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+            assertEquals(IpSecManager.Status.OK, createTransformResp.status);
+
+            verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp, udpSock.port);
+        } else {
+            try {
+                IpSecTransformResponse createTransformResp =
+                        mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+                fail("Expected IllegalArgumentException on attempt to use UDP Encap in IPv6");
+            } catch (IllegalArgumentException expected) {
+            }
+        }
+    }
+
+    @Test
+    public void testCreateTwoTransformsWithSameSpis() throws Exception {
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+        assertEquals(IpSecManager.Status.OK, createTransformResp.status);
+
+        // Attempting to create transform a second time with the same SPIs should throw an error...
+        try {
+            mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+                fail("IpSecService should have thrown an error for reuse of SPI");
+        } catch (IllegalStateException expected) {
+        }
+
+        // ... even if the transform is deleted
+        mIpSecService.deleteTransform(createTransformResp.resourceId);
+        try {
+            mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+                fail("IpSecService should have thrown an error for reuse of SPI");
+        } catch (IllegalStateException expected) {
+        }
+    }
+
+    @Test
+    public void testReleaseOwnedSpi() throws Exception {
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+        IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        assertEquals(1, userRecord.mSpiQuotaTracker.mCurrent);
+        mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId());
+        verify(mMockNetd, times(0))
+                .ipSecDeleteSecurityAssociation(
+                        eq(mUid),
+                        anyString(),
+                        anyString(),
+                        eq(TEST_SPI),
+                        anyInt(),
+                        anyInt(),
+                        anyInt());
+        // quota is not released until the SPI is released by the Transform
+        assertEquals(1, userRecord.mSpiQuotaTracker.mCurrent);
+    }
+
+    @Test
+    public void testDeleteTransform() throws Exception {
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+        mIpSecService.deleteTransform(createTransformResp.resourceId);
+
+        verify(mMockNetd, times(1))
+                .ipSecDeleteSecurityAssociation(
+                        eq(mUid),
+                        anyString(),
+                        anyString(),
+                        eq(TEST_SPI),
+                        anyInt(),
+                        anyInt(),
+                        anyInt());
+
+        // Verify quota and RefcountedResource objects cleaned up
+        IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        assertEquals(0, userRecord.mTransformQuotaTracker.mCurrent);
+        assertEquals(1, userRecord.mSpiQuotaTracker.mCurrent);
+
+        mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId());
+        // Verify that ipSecDeleteSa was not called when the SPI was released because the
+        // ownedByTransform property should prevent it; (note, the called count is cumulative).
+        verify(mMockNetd, times(1))
+                .ipSecDeleteSecurityAssociation(
+                        anyInt(),
+                        anyString(),
+                        anyString(),
+                        anyInt(),
+                        anyInt(),
+                        anyInt(),
+                        anyInt());
+        assertEquals(0, userRecord.mSpiQuotaTracker.mCurrent);
+
+        try {
+            userRecord.mTransformRecords.getRefcountedResourceOrThrow(
+                    createTransformResp.resourceId);
+            fail("Expected IllegalArgumentException on attempt to access deleted resource");
+        } catch (IllegalArgumentException expected) {
+
+        }
+    }
+
+    @Test
+    public void testTransportModeTransformBinderDeath() throws Exception {
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+
+        IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        IpSecService.RefcountedResource refcountedRecord =
+                userRecord.mTransformRecords.getRefcountedResourceOrThrow(
+                        createTransformResp.resourceId);
+
+        refcountedRecord.binderDied();
+
+        verify(mMockNetd)
+                .ipSecDeleteSecurityAssociation(
+                        eq(mUid),
+                        anyString(),
+                        anyString(),
+                        eq(TEST_SPI),
+                        anyInt(),
+                        anyInt(),
+                        anyInt());
+
+        // Verify quota and RefcountedResource objects cleaned up
+        assertEquals(0, userRecord.mTransformQuotaTracker.mCurrent);
+        try {
+            userRecord.mTransformRecords.getRefcountedResourceOrThrow(
+                    createTransformResp.resourceId);
+            fail("Expected IllegalArgumentException on attempt to access deleted resource");
+        } catch (IllegalArgumentException expected) {
+
+        }
+    }
+
+    @Test
+    public void testApplyTransportModeTransform() throws Exception {
+        verifyApplyTransportModeTransformCommon(false);
+    }
+
+    @Test
+    public void testApplyTransportModeTransformReleasedSpi() throws Exception {
+        verifyApplyTransportModeTransformCommon(true);
+    }
+
+    public void verifyApplyTransportModeTransformCommon(
+                boolean closeSpiBeforeApply) throws Exception {
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+
+        if (closeSpiBeforeApply) {
+            mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId());
+        }
+
+        Socket socket = new Socket();
+        socket.bind(null);
+        ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket);
+
+        int resourceId = createTransformResp.resourceId;
+        mIpSecService.applyTransportModeTransform(pfd, IpSecManager.DIRECTION_OUT, resourceId);
+
+        verify(mMockNetd)
+                .ipSecApplyTransportModeTransform(
+                        eq(pfd),
+                        eq(mUid),
+                        eq(IpSecManager.DIRECTION_OUT),
+                        anyString(),
+                        anyString(),
+                        eq(TEST_SPI));
+    }
+
+    @Test
+    public void testApplyTransportModeTransformWithClosedSpi() throws Exception {
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+
+        // Close SPI record
+        mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId());
+
+        Socket socket = new Socket();
+        socket.bind(null);
+        ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket);
+
+        int resourceId = createTransformResp.resourceId;
+        mIpSecService.applyTransportModeTransform(pfd, IpSecManager.DIRECTION_OUT, resourceId);
+
+        verify(mMockNetd)
+                .ipSecApplyTransportModeTransform(
+                        eq(pfd),
+                        eq(mUid),
+                        eq(IpSecManager.DIRECTION_OUT),
+                        anyString(),
+                        anyString(),
+                        eq(TEST_SPI));
+    }
+
+    @Test
+    public void testRemoveTransportModeTransform() throws Exception {
+        Socket socket = new Socket();
+        socket.bind(null);
+        ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket);
+        mIpSecService.removeTransportModeTransforms(pfd);
+
+        verify(mMockNetd).ipSecRemoveTransportModeTransform(pfd);
+    }
+
+    private IpSecTunnelInterfaceResponse createAndValidateTunnel(
+            String localAddr, String remoteAddr, String pkgName) throws Exception {
+        final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel();
+        config.flags = new String[] {IF_STATE_DOWN};
+        when(mMockNetd.interfaceGetCfg(anyString())).thenReturn(config);
+        IpSecTunnelInterfaceResponse createTunnelResp =
+                mIpSecService.createTunnelInterface(
+                        mSourceAddr, mDestinationAddr, fakeNetwork, new Binder(), pkgName);
+
+        assertNotNull(createTunnelResp);
+        assertEquals(IpSecManager.Status.OK, createTunnelResp.status);
+        for (int direction : new int[] {DIRECTION_IN, DIRECTION_OUT, DIRECTION_FWD}) {
+            for (int selAddrFamily : ADDRESS_FAMILIES) {
+                verify(mMockNetd).ipSecAddSecurityPolicy(
+                        eq(mUid),
+                        eq(selAddrFamily),
+                        eq(direction),
+                        anyString(),
+                        anyString(),
+                        eq(0),
+                        anyInt(), // iKey/oKey
+                        anyInt(), // mask
+                        eq(createTunnelResp.resourceId));
+            }
+        }
+
+        return createTunnelResp;
+    }
+
+    @Test
+    public void testCreateTunnelInterface() throws Exception {
+        IpSecTunnelInterfaceResponse createTunnelResp =
+                createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
+
+        // Check that we have stored the tracking object, and retrieve it
+        IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        IpSecService.RefcountedResource refcountedRecord =
+                userRecord.mTunnelInterfaceRecords.getRefcountedResourceOrThrow(
+                        createTunnelResp.resourceId);
+
+        assertEquals(1, userRecord.mTunnelQuotaTracker.mCurrent);
+        verify(mMockNetd)
+                .ipSecAddTunnelInterface(
+                        eq(createTunnelResp.interfaceName),
+                        eq(mSourceAddr),
+                        eq(mDestinationAddr),
+                        anyInt(),
+                        anyInt(),
+                        anyInt());
+        verify(mMockNetd).interfaceSetCfg(argThat(
+                config -> Arrays.asList(config.flags).contains(IF_STATE_UP)));
+    }
+
+    @Test
+    public void testDeleteTunnelInterface() throws Exception {
+        IpSecTunnelInterfaceResponse createTunnelResp =
+                createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
+
+        IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+
+        mIpSecService.deleteTunnelInterface(createTunnelResp.resourceId, BLESSED_PACKAGE);
+
+        // Verify quota and RefcountedResource objects cleaned up
+        assertEquals(0, userRecord.mTunnelQuotaTracker.mCurrent);
+        verify(mMockNetd).ipSecRemoveTunnelInterface(eq(createTunnelResp.interfaceName));
+        try {
+            userRecord.mTunnelInterfaceRecords.getRefcountedResourceOrThrow(
+                    createTunnelResp.resourceId);
+            fail("Expected IllegalArgumentException on attempt to access deleted resource");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    private Network createFakeUnderlyingNetwork(String interfaceName) {
+        final Network fakeNetwork = new Network(1000);
+        final LinkProperties fakeLp = new LinkProperties();
+        fakeLp.setInterfaceName(interfaceName);
+        when(mMockConnectivityMgr.getLinkProperties(eq(fakeNetwork))).thenReturn(fakeLp);
+        return fakeNetwork;
+    }
+
+    @Test
+    public void testSetNetworkForTunnelInterface() throws Exception {
+        final IpSecTunnelInterfaceResponse createTunnelResp =
+                createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
+        final Network newFakeNetwork = createFakeUnderlyingNetwork("newFakeNetworkInterface");
+        final int tunnelIfaceResourceId = createTunnelResp.resourceId;
+        mIpSecService.setNetworkForTunnelInterface(
+                tunnelIfaceResourceId, newFakeNetwork, BLESSED_PACKAGE);
+
+        final IpSecService.UserRecord userRecord =
+                mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        assertEquals(1, userRecord.mTunnelQuotaTracker.mCurrent);
+
+        final TunnelInterfaceRecord tunnelInterfaceInfo =
+                userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelIfaceResourceId);
+        assertEquals(newFakeNetwork, tunnelInterfaceInfo.getUnderlyingNetwork());
+    }
+
+    @Test
+    public void testSetNetworkForTunnelInterfaceFailsForInvalidResourceId() throws Exception {
+        final IpSecTunnelInterfaceResponse createTunnelResp =
+                createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
+        final Network newFakeNetwork = new Network(1000);
+
+        try {
+            mIpSecService.setNetworkForTunnelInterface(
+                    IpSecManager.INVALID_RESOURCE_ID, newFakeNetwork, BLESSED_PACKAGE);
+            fail("Expected an IllegalArgumentException for invalid resource ID.");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testSetNetworkForTunnelInterfaceFailsWhenSettingTunnelNetwork() throws Exception {
+        final IpSecTunnelInterfaceResponse createTunnelResp =
+                createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
+        final int tunnelIfaceResourceId = createTunnelResp.resourceId;
+        final IpSecService.UserRecord userRecord =
+                mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        final TunnelInterfaceRecord tunnelInterfaceInfo =
+                userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelIfaceResourceId);
+
+        final Network newFakeNetwork =
+                createFakeUnderlyingNetwork(tunnelInterfaceInfo.getInterfaceName());
+
+        try {
+            mIpSecService.setNetworkForTunnelInterface(
+                    tunnelIfaceResourceId, newFakeNetwork, BLESSED_PACKAGE);
+            fail(
+                    "Expected an IllegalArgumentException because the underlying network is the"
+                            + " network being exposed by this tunnel.");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testTunnelInterfaceBinderDeath() throws Exception {
+        IpSecTunnelInterfaceResponse createTunnelResp =
+                createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
+
+        IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid);
+        IpSecService.RefcountedResource refcountedRecord =
+                userRecord.mTunnelInterfaceRecords.getRefcountedResourceOrThrow(
+                        createTunnelResp.resourceId);
+
+        refcountedRecord.binderDied();
+
+        // Verify quota and RefcountedResource objects cleaned up
+        assertEquals(0, userRecord.mTunnelQuotaTracker.mCurrent);
+        verify(mMockNetd).ipSecRemoveTunnelInterface(eq(createTunnelResp.interfaceName));
+        try {
+            userRecord.mTunnelInterfaceRecords.getRefcountedResourceOrThrow(
+                    createTunnelResp.resourceId);
+            fail("Expected IllegalArgumentException on attempt to access deleted resource");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testApplyTunnelModeTransformOutbound() throws Exception {
+        verifyApplyTunnelModeTransformCommon(false /* closeSpiBeforeApply */, DIRECTION_OUT);
+    }
+
+    @Test
+    public void testApplyTunnelModeTransformOutboundNonNetworkStack() throws Exception {
+        mTestContext.setAllowedPermissions(android.Manifest.permission.MANAGE_IPSEC_TUNNELS);
+        verifyApplyTunnelModeTransformCommon(false /* closeSpiBeforeApply */, DIRECTION_OUT);
+    }
+
+    @Test
+    public void testApplyTunnelModeTransformOutboundReleasedSpi() throws Exception {
+        verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_OUT);
+    }
+
+    @Test
+    public void testApplyTunnelModeTransformInbound() throws Exception {
+        verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_IN);
+    }
+
+    @Test
+    public void testApplyTunnelModeTransformInboundNonNetworkStack() throws Exception {
+        mTestContext.setAllowedPermissions(android.Manifest.permission.MANAGE_IPSEC_TUNNELS);
+        verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_IN);
+    }
+
+    @Test
+    public void testApplyTunnelModeTransformForward() throws Exception {
+        verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_FWD);
+    }
+
+    @Test
+    public void testApplyTunnelModeTransformForwardNonNetworkStack() throws Exception {
+        mTestContext.setAllowedPermissions(android.Manifest.permission.MANAGE_IPSEC_TUNNELS);
+
+        try {
+            verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_FWD);
+            fail("Expected security exception due to use of forward policies without NETWORK_STACK"
+                     + " or MAINLINE_NETWORK_STACK permission");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    public void verifyApplyTunnelModeTransformCommon(boolean closeSpiBeforeApply, int direction)
+            throws Exception {
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        ipSecConfig.setMode(IpSecTransform.MODE_TUNNEL);
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+        IpSecTunnelInterfaceResponse createTunnelResp =
+                createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
+
+        if (closeSpiBeforeApply) {
+            mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId());
+        }
+
+        int transformResourceId = createTransformResp.resourceId;
+        int tunnelResourceId = createTunnelResp.resourceId;
+        mIpSecService.applyTunnelModeTransform(
+                tunnelResourceId, direction, transformResourceId, BLESSED_PACKAGE);
+
+        for (int selAddrFamily : ADDRESS_FAMILIES) {
+            verify(mMockNetd)
+                    .ipSecUpdateSecurityPolicy(
+                            eq(mUid),
+                            eq(selAddrFamily),
+                            eq(direction),
+                            anyString(),
+                            anyString(),
+                            eq(direction == DIRECTION_OUT ? TEST_SPI : 0),
+                            anyInt(), // iKey/oKey
+                            anyInt(), // mask
+                            eq(tunnelResourceId));
+        }
+
+        ipSecConfig.setXfrmInterfaceId(tunnelResourceId);
+        verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp);
+    }
+
+
+    @Test
+    public void testApplyTunnelModeTransformWithClosedSpi() throws Exception {
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        ipSecConfig.setMode(IpSecTransform.MODE_TUNNEL);
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+        IpSecTunnelInterfaceResponse createTunnelResp =
+                createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
+
+        // Close SPI record
+        mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId());
+
+        int transformResourceId = createTransformResp.resourceId;
+        int tunnelResourceId = createTunnelResp.resourceId;
+        mIpSecService.applyTunnelModeTransform(
+                tunnelResourceId, IpSecManager.DIRECTION_OUT, transformResourceId, BLESSED_PACKAGE);
+
+        for (int selAddrFamily : ADDRESS_FAMILIES) {
+            verify(mMockNetd)
+                    .ipSecUpdateSecurityPolicy(
+                            eq(mUid),
+                            eq(selAddrFamily),
+                            eq(IpSecManager.DIRECTION_OUT),
+                            anyString(),
+                            anyString(),
+                            eq(TEST_SPI),
+                            anyInt(), // iKey/oKey
+                            anyInt(), // mask
+                            eq(tunnelResourceId));
+        }
+
+        ipSecConfig.setXfrmInterfaceId(tunnelResourceId);
+        verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp);
+    }
+
+    @Test
+    public void testAddRemoveAddressFromTunnelInterface() throws Exception {
+        for (String pkgName : new String[] {BLESSED_PACKAGE, SYSTEM_PACKAGE}) {
+            IpSecTunnelInterfaceResponse createTunnelResp =
+                    createAndValidateTunnel(mSourceAddr, mDestinationAddr, pkgName);
+            mIpSecService.addAddressToTunnelInterface(
+                    createTunnelResp.resourceId, mLocalInnerAddress, pkgName);
+            verify(mMockNetd, times(1))
+                    .interfaceAddAddress(
+                            eq(createTunnelResp.interfaceName),
+                            eq(mLocalInnerAddress.getAddress().getHostAddress()),
+                            eq(mLocalInnerAddress.getPrefixLength()));
+            mIpSecService.removeAddressFromTunnelInterface(
+                    createTunnelResp.resourceId, mLocalInnerAddress, pkgName);
+            verify(mMockNetd, times(1))
+                    .interfaceDelAddress(
+                            eq(createTunnelResp.interfaceName),
+                            eq(mLocalInnerAddress.getAddress().getHostAddress()),
+                            eq(mLocalInnerAddress.getPrefixLength()));
+            mIpSecService.deleteTunnelInterface(createTunnelResp.resourceId, pkgName);
+        }
+    }
+
+    @Ignore
+    @Test
+    public void testAddTunnelFailsForBadPackageName() throws Exception {
+        try {
+            IpSecTunnelInterfaceResponse createTunnelResp =
+                    createAndValidateTunnel(mSourceAddr, mDestinationAddr, BAD_PACKAGE);
+            fail("Expected a SecurityException for badPackage.");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
+    public void testFeatureFlagVerification() throws Exception {
+        when(mMockPkgMgr.hasSystemFeature(eq(PackageManager.FEATURE_IPSEC_TUNNELS)))
+                .thenReturn(false);
+
+        try {
+            String addr = Inet4Address.getLoopbackAddress().getHostAddress();
+            mIpSecService.createTunnelInterface(
+                    addr, addr, new Network(0), new Binder(), BLESSED_PACKAGE);
+            fail("Expected UnsupportedOperationException for disabled feature");
+        } catch (UnsupportedOperationException expected) {
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
new file mode 100644
index 0000000..22a2c94
--- /dev/null
+++ b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.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 com.android.server;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.IpSecService.IResource;
+import com.android.server.IpSecService.RefcountedResource;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
+
+/** Unit tests for {@link IpSecService.RefcountedResource}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class IpSecServiceRefcountedResourceTest {
+    Context mMockContext;
+    IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig;
+    IpSecService mIpSecService;
+
+    @Before
+    public void setUp() throws Exception {
+        mMockContext = mock(Context.class);
+        mMockIpSecSrvConfig = mock(IpSecService.IpSecServiceConfiguration.class);
+        mIpSecService = new IpSecService(mMockContext, mMockIpSecSrvConfig);
+    }
+
+    private void assertResourceState(
+            RefcountedResource<IResource> resource,
+            int refCount,
+            int userReleaseCallCount,
+            int releaseReferenceCallCount,
+            int invalidateCallCount,
+            int freeUnderlyingResourcesCallCount)
+            throws RemoteException {
+        // Check refcount on RefcountedResource
+        assertEquals(refCount, resource.mRefCount);
+
+        // Check call count of RefcountedResource
+        verify(resource, times(userReleaseCallCount)).userRelease();
+        verify(resource, times(releaseReferenceCallCount)).releaseReference();
+
+        // Check call count of IResource
+        verify(resource.getResource(), times(invalidateCallCount)).invalidate();
+        verify(resource.getResource(), times(freeUnderlyingResourcesCallCount))
+                .freeUnderlyingResources();
+    }
+
+    /** Adds mockito instrumentation */
+    private RefcountedResource<IResource> getTestRefcountedResource(
+            RefcountedResource... children) {
+        return getTestRefcountedResource(new Binder(), children);
+    }
+
+    /** Adds mockito instrumentation with provided binder */
+    private RefcountedResource<IResource> getTestRefcountedResource(
+            IBinder binder, RefcountedResource... children) {
+        return spy(
+                mIpSecService
+                .new RefcountedResource<IResource>(mock(IResource.class), binder, children));
+    }
+
+    @Test
+    public void testConstructor() throws RemoteException {
+        IBinder binderMock = mock(IBinder.class);
+        RefcountedResource<IResource> resource = getTestRefcountedResource(binderMock);
+
+        // Verify resource's refcount starts at 1 (for user-reference)
+        assertResourceState(resource, 1, 0, 0, 0, 0);
+
+        // Verify linking to binder death
+        verify(binderMock).linkToDeath(anyObject(), anyInt());
+    }
+
+    @Test
+    public void testConstructorWithChildren() throws RemoteException {
+        IBinder binderMockChild = mock(IBinder.class);
+        IBinder binderMockParent = mock(IBinder.class);
+        RefcountedResource<IResource> childResource = getTestRefcountedResource(binderMockChild);
+        RefcountedResource<IResource> parentResource =
+                getTestRefcountedResource(binderMockParent, childResource);
+
+        // Verify parent's refcount starts at 1 (for user-reference)
+        assertResourceState(parentResource, 1, 0, 0, 0, 0);
+
+        // Verify child's refcounts were incremented
+        assertResourceState(childResource, 2, 0, 0, 0, 0);
+
+        // Verify linking to binder death
+        verify(binderMockChild).linkToDeath(anyObject(), anyInt());
+        verify(binderMockParent).linkToDeath(anyObject(), anyInt());
+    }
+
+    @Test
+    public void testFailLinkToDeath() throws RemoteException {
+        IBinder binderMock = mock(IBinder.class);
+        doThrow(new RemoteException()).when(binderMock).linkToDeath(anyObject(), anyInt());
+
+        try {
+            getTestRefcountedResource(binderMock);
+            fail("Expected exception to propogate when binder fails to link to death");
+        } catch (RuntimeException expected) {
+        }
+    }
+
+    @Test
+    public void testCleanupAndRelease() throws RemoteException {
+        IBinder binderMock = mock(IBinder.class);
+        RefcountedResource<IResource> refcountedResource = getTestRefcountedResource(binderMock);
+
+        // Verify user-initiated cleanup path decrements refcount and calls full cleanup flow
+        refcountedResource.userRelease();
+        assertResourceState(refcountedResource, -1, 1, 1, 1, 1);
+
+        // Verify user-initated cleanup path unlinks from binder
+        verify(binderMock).unlinkToDeath(eq(refcountedResource), eq(0));
+        assertNull(refcountedResource.mBinder);
+    }
+
+    @Test
+    public void testMultipleCallsToCleanupAndRelease() throws RemoteException {
+        RefcountedResource<IResource> refcountedResource = getTestRefcountedResource();
+
+        // Verify calling userRelease multiple times does not trigger any other cleanup
+        // methods
+        refcountedResource.userRelease();
+        assertResourceState(refcountedResource, -1, 1, 1, 1, 1);
+
+        refcountedResource.userRelease();
+        refcountedResource.userRelease();
+        assertResourceState(refcountedResource, -1, 3, 1, 1, 1);
+    }
+
+    @Test
+    public void testBinderDeathAfterCleanupAndReleaseDoesNothing() throws RemoteException {
+        RefcountedResource<IResource> refcountedResource = getTestRefcountedResource();
+
+        refcountedResource.userRelease();
+        assertResourceState(refcountedResource, -1, 1, 1, 1, 1);
+
+        // Verify binder death call does not trigger any other cleanup methods if called after
+        // userRelease()
+        refcountedResource.binderDied();
+        assertResourceState(refcountedResource, -1, 2, 1, 1, 1);
+    }
+
+    @Test
+    public void testBinderDeath() throws RemoteException {
+        RefcountedResource<IResource> refcountedResource = getTestRefcountedResource();
+
+        // Verify binder death caused cleanup
+        refcountedResource.binderDied();
+        verify(refcountedResource, times(1)).binderDied();
+        assertResourceState(refcountedResource, -1, 1, 1, 1, 1);
+        assertNull(refcountedResource.mBinder);
+    }
+
+    @Test
+    public void testCleanupParentDecrementsChildRefcount() throws RemoteException {
+        RefcountedResource<IResource> childResource = getTestRefcountedResource();
+        RefcountedResource<IResource> parentResource = getTestRefcountedResource(childResource);
+
+        parentResource.userRelease();
+
+        // Verify parent gets cleaned up properly, and triggers releaseReference on
+        // child
+        assertResourceState(childResource, 1, 0, 1, 0, 0);
+        assertResourceState(parentResource, -1, 1, 1, 1, 1);
+    }
+
+    @Test
+    public void testCleanupReferencedChildDoesNotTriggerRelease() throws RemoteException {
+        RefcountedResource<IResource> childResource = getTestRefcountedResource();
+        RefcountedResource<IResource> parentResource = getTestRefcountedResource(childResource);
+
+        childResource.userRelease();
+
+        // Verify that child does not clean up kernel resources and quota.
+        assertResourceState(childResource, 1, 1, 1, 1, 0);
+        assertResourceState(parentResource, 1, 0, 0, 0, 0);
+    }
+
+    @Test
+    public void testTwoParents() throws RemoteException {
+        RefcountedResource<IResource> childResource = getTestRefcountedResource();
+        RefcountedResource<IResource> parentResource1 = getTestRefcountedResource(childResource);
+        RefcountedResource<IResource> parentResource2 = getTestRefcountedResource(childResource);
+
+        // Verify that child does not cleanup kernel resources and quota until all references
+        // have been released. Assumption: parents release correctly based on
+        // testCleanupParentDecrementsChildRefcount()
+        childResource.userRelease();
+        assertResourceState(childResource, 2, 1, 1, 1, 0);
+
+        parentResource1.userRelease();
+        assertResourceState(childResource, 1, 1, 2, 1, 0);
+
+        parentResource2.userRelease();
+        assertResourceState(childResource, -1, 1, 3, 1, 1);
+    }
+
+    @Test
+    public void testTwoChildren() throws RemoteException {
+        RefcountedResource<IResource> childResource1 = getTestRefcountedResource();
+        RefcountedResource<IResource> childResource2 = getTestRefcountedResource();
+        RefcountedResource<IResource> parentResource =
+                getTestRefcountedResource(childResource1, childResource2);
+
+        childResource1.userRelease();
+        assertResourceState(childResource1, 1, 1, 1, 1, 0);
+        assertResourceState(childResource2, 2, 0, 0, 0, 0);
+
+        parentResource.userRelease();
+        assertResourceState(childResource1, -1, 1, 2, 1, 1);
+        assertResourceState(childResource2, 1, 0, 1, 0, 0);
+
+        childResource2.userRelease();
+        assertResourceState(childResource1, -1, 1, 2, 1, 1);
+        assertResourceState(childResource2, -1, 1, 2, 1, 1);
+    }
+
+    @Test
+    public void testSampleUdpEncapTranform() throws RemoteException {
+        RefcountedResource<IResource> spi1 = getTestRefcountedResource();
+        RefcountedResource<IResource> spi2 = getTestRefcountedResource();
+        RefcountedResource<IResource> udpEncapSocket = getTestRefcountedResource();
+        RefcountedResource<IResource> transform =
+                getTestRefcountedResource(spi1, spi2, udpEncapSocket);
+
+        // Pretend one SPI goes out of reference (releaseManagedResource -> userRelease)
+        spi1.userRelease();
+
+        // User called releaseManagedResource on udpEncap socket
+        udpEncapSocket.userRelease();
+
+        // User dies, and binder kills the rest
+        spi2.binderDied();
+        transform.binderDied();
+
+        // Check resource states
+        assertResourceState(spi1, -1, 1, 2, 1, 1);
+        assertResourceState(spi2, -1, 1, 2, 1, 1);
+        assertResourceState(udpEncapSocket, -1, 1, 2, 1, 1);
+        assertResourceState(transform, -1, 1, 1, 1, 1);
+    }
+
+    @Test
+    public void testSampleDualTransformEncapSocket() throws RemoteException {
+        RefcountedResource<IResource> spi1 = getTestRefcountedResource();
+        RefcountedResource<IResource> spi2 = getTestRefcountedResource();
+        RefcountedResource<IResource> spi3 = getTestRefcountedResource();
+        RefcountedResource<IResource> spi4 = getTestRefcountedResource();
+        RefcountedResource<IResource> udpEncapSocket = getTestRefcountedResource();
+        RefcountedResource<IResource> transform1 =
+                getTestRefcountedResource(spi1, spi2, udpEncapSocket);
+        RefcountedResource<IResource> transform2 =
+                getTestRefcountedResource(spi3, spi4, udpEncapSocket);
+
+        // Pretend one SPIs goes out of reference (releaseManagedResource -> userRelease)
+        spi1.userRelease();
+
+        // User called releaseManagedResource on udpEncap socket and spi4
+        udpEncapSocket.userRelease();
+        spi4.userRelease();
+
+        // User dies, and binder kills the rest
+        spi2.binderDied();
+        spi3.binderDied();
+        transform2.binderDied();
+        transform1.binderDied();
+
+        // Check resource states
+        assertResourceState(spi1, -1, 1, 2, 1, 1);
+        assertResourceState(spi2, -1, 1, 2, 1, 1);
+        assertResourceState(spi3, -1, 1, 2, 1, 1);
+        assertResourceState(spi4, -1, 1, 2, 1, 1);
+        assertResourceState(udpEncapSocket, -1, 1, 3, 1, 1);
+        assertResourceState(transform1, -1, 1, 1, 1, 1);
+        assertResourceState(transform2, -1, 1, 1, 1, 1);
+    }
+
+    @Test
+    public void fuzzTest() throws RemoteException {
+        List<RefcountedResource<IResource>> resources = new ArrayList<>();
+
+        // Build a tree of resources
+        for (int i = 0; i < 100; i++) {
+            // Choose a random number of children from the existing list
+            int numChildren = ThreadLocalRandom.current().nextInt(0, resources.size() + 1);
+
+            // Build a (random) list of children
+            Set<RefcountedResource<IResource>> children = new HashSet<>();
+            for (int j = 0; j < numChildren; j++) {
+                int childIndex = ThreadLocalRandom.current().nextInt(0, resources.size());
+                children.add(resources.get(childIndex));
+            }
+
+            RefcountedResource<IResource> newRefcountedResource =
+                    getTestRefcountedResource(
+                            children.toArray(new RefcountedResource[children.size()]));
+            resources.add(newRefcountedResource);
+        }
+
+        // Cleanup all resources in a random order
+        List<RefcountedResource<IResource>> clonedResources =
+                new ArrayList<>(resources); // shallow copy
+        while (!clonedResources.isEmpty()) {
+            int index = ThreadLocalRandom.current().nextInt(0, clonedResources.size());
+            RefcountedResource<IResource> refcountedResource = clonedResources.get(index);
+            refcountedResource.userRelease();
+            clonedResources.remove(index);
+        }
+
+        // Verify all resources were cleaned up properly
+        for (RefcountedResource<IResource> refcountedResource : resources) {
+            assertEquals(-1, refcountedResource.mRefCount);
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/IpSecServiceTest.java b/tests/unit/java/com/android/server/IpSecServiceTest.java
new file mode 100644
index 0000000..6232423
--- /dev/null
+++ b/tests/unit/java/com/android/server/IpSecServiceTest.java
@@ -0,0 +1,670 @@
+/*
+ * 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.system.OsConstants.AF_INET;
+import static android.system.OsConstants.EADDRINUSE;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.IpSecAlgorithm;
+import android.net.IpSecConfig;
+import android.net.IpSecManager;
+import android.net.IpSecSpiResponse;
+import android.net.IpSecUdpEncapResponse;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructStat;
+import android.util.Range;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import dalvik.system.SocketTagger;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Unit tests for {@link IpSecService}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class IpSecServiceTest {
+
+    private static final int DROID_SPI = 0xD1201D;
+    private static final int MAX_NUM_ENCAP_SOCKETS = 100;
+    private static final int MAX_NUM_SPIS = 100;
+    private static final int TEST_UDP_ENCAP_INVALID_PORT = 100;
+    private static final int TEST_UDP_ENCAP_PORT_OUT_RANGE = 100000;
+
+    private static final InetAddress INADDR_ANY;
+
+    private static final byte[] AEAD_KEY = {
+        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+        0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+        0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+        0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
+        0x73, 0x61, 0x6C, 0x74
+    };
+    private static final byte[] CRYPT_KEY = {
+        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+        0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+        0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+        0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F
+    };
+    private static final byte[] AUTH_KEY = {
+        0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F,
+        0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F
+    };
+
+    private static final IpSecAlgorithm AUTH_ALGO =
+            new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4);
+    private static final IpSecAlgorithm CRYPT_ALGO =
+            new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY);
+    private static final IpSecAlgorithm AEAD_ALGO =
+            new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128);
+
+    static {
+        try {
+            INADDR_ANY = InetAddress.getByAddress(new byte[] {0, 0, 0, 0});
+        } catch (UnknownHostException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    Context mMockContext;
+    INetd mMockNetd;
+    IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig;
+    IpSecService mIpSecService;
+
+    @Before
+    public void setUp() throws Exception {
+        mMockContext = mock(Context.class);
+        mMockNetd = mock(INetd.class);
+        mMockIpSecSrvConfig = mock(IpSecService.IpSecServiceConfiguration.class);
+        mIpSecService = new IpSecService(mMockContext, mMockIpSecSrvConfig);
+
+        // Injecting mock netd
+        when(mMockIpSecSrvConfig.getNetdInstance()).thenReturn(mMockNetd);
+    }
+
+    @Test
+    public void testIpSecServiceCreate() throws InterruptedException {
+        IpSecService ipSecSrv = IpSecService.create(mMockContext);
+        assertNotNull(ipSecSrv);
+    }
+
+    @Test
+    public void testReleaseInvalidSecurityParameterIndex() throws Exception {
+        try {
+            mIpSecService.releaseSecurityParameterIndex(1);
+            fail("IllegalArgumentException not thrown");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    /** This function finds an available port */
+    int findUnusedPort() throws Exception {
+        // Get an available port.
+        ServerSocket s = new ServerSocket(0);
+        int port = s.getLocalPort();
+        s.close();
+        return port;
+    }
+
+    @Test
+    public void testOpenAndCloseUdpEncapsulationSocket() throws Exception {
+        int localport = -1;
+        IpSecUdpEncapResponse udpEncapResp = null;
+
+        for (int i = 0; i < IpSecService.MAX_PORT_BIND_ATTEMPTS; i++) {
+            localport = findUnusedPort();
+
+            udpEncapResp = mIpSecService.openUdpEncapsulationSocket(localport, new Binder());
+            assertNotNull(udpEncapResp);
+            if (udpEncapResp.status == IpSecManager.Status.OK) {
+                break;
+            }
+
+            // Else retry to reduce possibility for port-bind failures.
+        }
+
+        assertNotNull(udpEncapResp);
+        assertEquals(IpSecManager.Status.OK, udpEncapResp.status);
+        assertEquals(localport, udpEncapResp.port);
+
+        mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId);
+        udpEncapResp.fileDescriptor.close();
+
+        // Verify quota and RefcountedResource objects cleaned up
+        IpSecService.UserRecord userRecord =
+                mIpSecService.mUserResourceTracker.getUserRecord(Os.getuid());
+        assertEquals(0, userRecord.mSocketQuotaTracker.mCurrent);
+        try {
+            userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(udpEncapResp.resourceId);
+            fail("Expected IllegalArgumentException on attempt to access deleted resource");
+        } catch (IllegalArgumentException expected) {
+
+        }
+    }
+
+    @Test
+    public void testUdpEncapsulationSocketBinderDeath() throws Exception {
+        IpSecUdpEncapResponse udpEncapResp =
+                mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+
+        IpSecService.UserRecord userRecord =
+                mIpSecService.mUserResourceTracker.getUserRecord(Os.getuid());
+        IpSecService.RefcountedResource refcountedRecord =
+                userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(
+                        udpEncapResp.resourceId);
+
+        refcountedRecord.binderDied();
+
+        // Verify quota and RefcountedResource objects cleaned up
+        assertEquals(0, userRecord.mSocketQuotaTracker.mCurrent);
+        try {
+            userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(udpEncapResp.resourceId);
+            fail("Expected IllegalArgumentException on attempt to access deleted resource");
+        } catch (IllegalArgumentException expected) {
+
+        }
+    }
+
+    @Test
+    public void testOpenUdpEncapsulationSocketAfterClose() throws Exception {
+        IpSecUdpEncapResponse udpEncapResp =
+                mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+        assertNotNull(udpEncapResp);
+        assertEquals(IpSecManager.Status.OK, udpEncapResp.status);
+        int localport = udpEncapResp.port;
+
+        mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId);
+        udpEncapResp.fileDescriptor.close();
+
+        /** Check if localport is available. */
+        FileDescriptor newSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+        Os.bind(newSocket, INADDR_ANY, localport);
+        Os.close(newSocket);
+    }
+
+    /**
+     * This function checks if the IpSecService holds the reserved port. If
+     * closeUdpEncapsulationSocket is not called, the socket cleanup should not be complete.
+     */
+    @Test
+    public void testUdpEncapPortNotReleased() throws Exception {
+        IpSecUdpEncapResponse udpEncapResp =
+                mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+        assertNotNull(udpEncapResp);
+        assertEquals(IpSecManager.Status.OK, udpEncapResp.status);
+        int localport = udpEncapResp.port;
+
+        udpEncapResp.fileDescriptor.close();
+
+        FileDescriptor newSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+        try {
+            Os.bind(newSocket, INADDR_ANY, localport);
+            fail("ErrnoException not thrown");
+        } catch (ErrnoException e) {
+            assertEquals(EADDRINUSE, e.errno);
+        }
+        mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId);
+    }
+
+    @Test
+    public void testOpenUdpEncapsulationSocketOnRandomPort() throws Exception {
+        IpSecUdpEncapResponse udpEncapResp =
+                mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+        assertNotNull(udpEncapResp);
+        assertEquals(IpSecManager.Status.OK, udpEncapResp.status);
+        assertNotEquals(0, udpEncapResp.port);
+        mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId);
+        udpEncapResp.fileDescriptor.close();
+    }
+
+    @Test
+    public void testOpenUdpEncapsulationSocketPortRange() throws Exception {
+        try {
+            mIpSecService.openUdpEncapsulationSocket(TEST_UDP_ENCAP_INVALID_PORT, new Binder());
+            fail("IllegalArgumentException not thrown");
+        } catch (IllegalArgumentException e) {
+        }
+
+        try {
+            mIpSecService.openUdpEncapsulationSocket(TEST_UDP_ENCAP_PORT_OUT_RANGE, new Binder());
+            fail("IllegalArgumentException not thrown");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Test
+    public void testOpenUdpEncapsulationSocketTwice() throws Exception {
+        IpSecUdpEncapResponse udpEncapResp =
+                mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+        assertNotNull(udpEncapResp);
+        assertEquals(IpSecManager.Status.OK, udpEncapResp.status);
+        int localport = udpEncapResp.port;
+
+        IpSecUdpEncapResponse testUdpEncapResp =
+                mIpSecService.openUdpEncapsulationSocket(localport, new Binder());
+        assertEquals(IpSecManager.Status.RESOURCE_UNAVAILABLE, testUdpEncapResp.status);
+
+        mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId);
+        udpEncapResp.fileDescriptor.close();
+    }
+
+    @Test
+    public void testCloseInvalidUdpEncapsulationSocket() throws Exception {
+        try {
+            mIpSecService.closeUdpEncapsulationSocket(1);
+            fail("IllegalArgumentException not thrown");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Test
+    public void testValidateAlgorithmsAuth() {
+        // Validate that correct algorithm type succeeds
+        IpSecConfig config = new IpSecConfig();
+        config.setAuthentication(AUTH_ALGO);
+        mIpSecService.validateAlgorithms(config);
+
+        // Validate that incorrect algorithm types fails
+        for (IpSecAlgorithm algo : new IpSecAlgorithm[] {CRYPT_ALGO, AEAD_ALGO}) {
+            try {
+                config = new IpSecConfig();
+                config.setAuthentication(algo);
+                mIpSecService.validateAlgorithms(config);
+                fail("Did not throw exception on invalid algorithm type");
+            } catch (IllegalArgumentException expected) {
+            }
+        }
+    }
+
+    @Test
+    public void testValidateAlgorithmsCrypt() {
+        // Validate that correct algorithm type succeeds
+        IpSecConfig config = new IpSecConfig();
+        config.setEncryption(CRYPT_ALGO);
+        mIpSecService.validateAlgorithms(config);
+
+        // Validate that incorrect algorithm types fails
+        for (IpSecAlgorithm algo : new IpSecAlgorithm[] {AUTH_ALGO, AEAD_ALGO}) {
+            try {
+                config = new IpSecConfig();
+                config.setEncryption(algo);
+                mIpSecService.validateAlgorithms(config);
+                fail("Did not throw exception on invalid algorithm type");
+            } catch (IllegalArgumentException expected) {
+            }
+        }
+    }
+
+    @Test
+    public void testValidateAlgorithmsAead() {
+        // Validate that correct algorithm type succeeds
+        IpSecConfig config = new IpSecConfig();
+        config.setAuthenticatedEncryption(AEAD_ALGO);
+        mIpSecService.validateAlgorithms(config);
+
+        // Validate that incorrect algorithm types fails
+        for (IpSecAlgorithm algo : new IpSecAlgorithm[] {AUTH_ALGO, CRYPT_ALGO}) {
+            try {
+                config = new IpSecConfig();
+                config.setAuthenticatedEncryption(algo);
+                mIpSecService.validateAlgorithms(config);
+                fail("Did not throw exception on invalid algorithm type");
+            } catch (IllegalArgumentException expected) {
+            }
+        }
+    }
+
+    @Test
+    public void testValidateAlgorithmsAuthCrypt() {
+        // Validate that correct algorithm type succeeds
+        IpSecConfig config = new IpSecConfig();
+        config.setAuthentication(AUTH_ALGO);
+        config.setEncryption(CRYPT_ALGO);
+        mIpSecService.validateAlgorithms(config);
+    }
+
+    @Test
+    public void testValidateAlgorithmsNoAlgorithms() {
+        IpSecConfig config = new IpSecConfig();
+        try {
+            mIpSecService.validateAlgorithms(config);
+            fail("Expected exception; no algorithms specified");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testValidateAlgorithmsAeadWithAuth() {
+        IpSecConfig config = new IpSecConfig();
+        config.setAuthenticatedEncryption(AEAD_ALGO);
+        config.setAuthentication(AUTH_ALGO);
+        try {
+            mIpSecService.validateAlgorithms(config);
+            fail("Expected exception; both AEAD and auth algorithm specified");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testValidateAlgorithmsAeadWithCrypt() {
+        IpSecConfig config = new IpSecConfig();
+        config.setAuthenticatedEncryption(AEAD_ALGO);
+        config.setEncryption(CRYPT_ALGO);
+        try {
+            mIpSecService.validateAlgorithms(config);
+            fail("Expected exception; both AEAD and crypt algorithm specified");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testValidateAlgorithmsAeadWithAuthAndCrypt() {
+        IpSecConfig config = new IpSecConfig();
+        config.setAuthenticatedEncryption(AEAD_ALGO);
+        config.setAuthentication(AUTH_ALGO);
+        config.setEncryption(CRYPT_ALGO);
+        try {
+            mIpSecService.validateAlgorithms(config);
+            fail("Expected exception; AEAD, auth and crypt algorithm specified");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testDeleteInvalidTransform() throws Exception {
+        try {
+            mIpSecService.deleteTransform(1);
+            fail("IllegalArgumentException not thrown");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Test
+    public void testRemoveTransportModeTransform() throws Exception {
+        Socket socket = new Socket();
+        socket.bind(null);
+        ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket);
+        mIpSecService.removeTransportModeTransforms(pfd);
+
+        verify(mMockNetd).ipSecRemoveTransportModeTransform(pfd);
+    }
+
+    @Test
+    public void testValidateIpAddresses() throws Exception {
+        String[] invalidAddresses =
+                new String[] {"www.google.com", "::", "2001::/64", "0.0.0.0", ""};
+        for (String address : invalidAddresses) {
+            try {
+                IpSecSpiResponse spiResp =
+                        mIpSecService.allocateSecurityParameterIndex(
+                                address, DROID_SPI, new Binder());
+                fail("Invalid address was passed through IpSecService validation: " + address);
+            } catch (IllegalArgumentException e) {
+            } catch (Exception e) {
+                fail(
+                        "Invalid InetAddress was not caught in validation: "
+                                + address
+                                + ", Exception: "
+                                + e);
+            }
+        }
+    }
+
+    /**
+     * This function checks if the number of encap UDP socket that one UID can reserve has a
+     * reasonable limit.
+     */
+    @Test
+    public void testSocketResourceTrackerLimitation() throws Exception {
+        List<IpSecUdpEncapResponse> openUdpEncapSockets = new ArrayList<IpSecUdpEncapResponse>();
+        // Reserve sockets until it fails.
+        for (int i = 0; i < MAX_NUM_ENCAP_SOCKETS; i++) {
+            IpSecUdpEncapResponse newUdpEncapSocket =
+                    mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+            assertNotNull(newUdpEncapSocket);
+            if (IpSecManager.Status.OK != newUdpEncapSocket.status) {
+                break;
+            }
+            openUdpEncapSockets.add(newUdpEncapSocket);
+        }
+        // Assert that the total sockets quota has a reasonable limit.
+        assertTrue("No UDP encap socket was open", !openUdpEncapSockets.isEmpty());
+        assertTrue(
+                "Number of open UDP encap sockets is out of bound",
+                openUdpEncapSockets.size() < MAX_NUM_ENCAP_SOCKETS);
+
+        // Try to reserve one more UDP encapsulation socket, and should fail.
+        IpSecUdpEncapResponse extraUdpEncapSocket =
+                mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+        assertNotNull(extraUdpEncapSocket);
+        assertEquals(IpSecManager.Status.RESOURCE_UNAVAILABLE, extraUdpEncapSocket.status);
+
+        // Close one of the open UDP encapsulation sockets.
+        mIpSecService.closeUdpEncapsulationSocket(openUdpEncapSockets.get(0).resourceId);
+        openUdpEncapSockets.get(0).fileDescriptor.close();
+        openUdpEncapSockets.remove(0);
+
+        // Try to reserve one more UDP encapsulation socket, and should be successful.
+        extraUdpEncapSocket = mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+        assertNotNull(extraUdpEncapSocket);
+        assertEquals(IpSecManager.Status.OK, extraUdpEncapSocket.status);
+        openUdpEncapSockets.add(extraUdpEncapSocket);
+
+        // Close open UDP sockets.
+        for (IpSecUdpEncapResponse openSocket : openUdpEncapSockets) {
+            mIpSecService.closeUdpEncapsulationSocket(openSocket.resourceId);
+            openSocket.fileDescriptor.close();
+        }
+    }
+
+    /**
+     * This function checks if the number of SPI that one UID can reserve has a reasonable limit.
+     * This test does not test for both address families or duplicate SPIs because resource tracking
+     * code does not depend on them.
+     */
+    @Test
+    public void testSpiResourceTrackerLimitation() throws Exception {
+        List<IpSecSpiResponse> reservedSpis = new ArrayList<IpSecSpiResponse>();
+        // Return the same SPI for all SPI allocation since IpSecService only
+        // tracks the resource ID.
+        when(mMockNetd.ipSecAllocateSpi(
+                        anyInt(),
+                        anyString(),
+                        eq(InetAddress.getLoopbackAddress().getHostAddress()),
+                        anyInt()))
+                .thenReturn(DROID_SPI);
+        // Reserve spis until it fails.
+        for (int i = 0; i < MAX_NUM_SPIS; i++) {
+            IpSecSpiResponse newSpi =
+                    mIpSecService.allocateSecurityParameterIndex(
+                            InetAddress.getLoopbackAddress().getHostAddress(),
+                            DROID_SPI + i,
+                            new Binder());
+            assertNotNull(newSpi);
+            if (IpSecManager.Status.OK != newSpi.status) {
+                break;
+            }
+            reservedSpis.add(newSpi);
+        }
+        // Assert that the SPI quota has a reasonable limit.
+        assertTrue(reservedSpis.size() > 0 && reservedSpis.size() < MAX_NUM_SPIS);
+
+        // Try to reserve one more SPI, and should fail.
+        IpSecSpiResponse extraSpi =
+                mIpSecService.allocateSecurityParameterIndex(
+                        InetAddress.getLoopbackAddress().getHostAddress(),
+                        DROID_SPI + MAX_NUM_SPIS,
+                        new Binder());
+        assertNotNull(extraSpi);
+        assertEquals(IpSecManager.Status.RESOURCE_UNAVAILABLE, extraSpi.status);
+
+        // Release one reserved spi.
+        mIpSecService.releaseSecurityParameterIndex(reservedSpis.get(0).resourceId);
+        reservedSpis.remove(0);
+
+        // Should successfully reserve one more spi.
+        extraSpi =
+                mIpSecService.allocateSecurityParameterIndex(
+                        InetAddress.getLoopbackAddress().getHostAddress(),
+                        DROID_SPI + MAX_NUM_SPIS,
+                        new Binder());
+        assertNotNull(extraSpi);
+        assertEquals(IpSecManager.Status.OK, extraSpi.status);
+
+        // Release reserved SPIs.
+        for (IpSecSpiResponse spiResp : reservedSpis) {
+            mIpSecService.releaseSecurityParameterIndex(spiResp.resourceId);
+        }
+    }
+
+    @Test
+    public void testUidFdtagger() throws Exception {
+        SocketTagger actualSocketTagger = SocketTagger.get();
+
+        try {
+            FileDescriptor sockFd = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+
+            // Has to be done after socket creation because BlockGuardOS calls tag on new sockets
+            SocketTagger mockSocketTagger = mock(SocketTagger.class);
+            SocketTagger.set(mockSocketTagger);
+
+            mIpSecService.mUidFdTagger.tag(sockFd, Process.LAST_APPLICATION_UID);
+            verify(mockSocketTagger).tag(eq(sockFd));
+        } finally {
+            SocketTagger.set(actualSocketTagger);
+        }
+    }
+
+    /**
+     * Checks if two file descriptors point to the same file.
+     *
+     * <p>According to stat.h documentation, the correct way to check for equivalent or duplicated
+     * file descriptors is to check their inode and device. These two entries uniquely identify any
+     * file.
+     */
+    private boolean fileDescriptorsEqual(FileDescriptor fd1, FileDescriptor fd2) {
+        try {
+            StructStat fd1Stat = Os.fstat(fd1);
+            StructStat fd2Stat = Os.fstat(fd2);
+
+            return fd1Stat.st_ino == fd2Stat.st_ino && fd1Stat.st_dev == fd2Stat.st_dev;
+        } catch (ErrnoException e) {
+            return false;
+        }
+    }
+
+    @Test
+    public void testOpenUdpEncapSocketTagsSocket() throws Exception {
+        IpSecService.UidFdTagger mockTagger = mock(IpSecService.UidFdTagger.class);
+        IpSecService testIpSecService = new IpSecService(
+                mMockContext, mMockIpSecSrvConfig, mockTagger);
+
+        IpSecUdpEncapResponse udpEncapResp =
+                testIpSecService.openUdpEncapsulationSocket(0, new Binder());
+        assertNotNull(udpEncapResp);
+        assertEquals(IpSecManager.Status.OK, udpEncapResp.status);
+
+        FileDescriptor sockFd = udpEncapResp.fileDescriptor.getFileDescriptor();
+        ArgumentMatcher<FileDescriptor> fdMatcher =
+                (argFd) -> {
+                    return fileDescriptorsEqual(sockFd, argFd);
+                };
+        verify(mockTagger).tag(argThat(fdMatcher), eq(Os.getuid()));
+
+        testIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId);
+        udpEncapResp.fileDescriptor.close();
+    }
+
+    @Test
+    public void testOpenUdpEncapsulationSocketCallsSetEncapSocketOwner() throws Exception {
+        IpSecUdpEncapResponse udpEncapResp =
+                mIpSecService.openUdpEncapsulationSocket(0, new Binder());
+
+        FileDescriptor sockFd = udpEncapResp.fileDescriptor.getFileDescriptor();
+        ArgumentMatcher<ParcelFileDescriptor> fdMatcher = (arg) -> {
+                    try {
+                        StructStat sockStat = Os.fstat(sockFd);
+                        StructStat argStat = Os.fstat(arg.getFileDescriptor());
+
+                        return sockStat.st_ino == argStat.st_ino
+                                && sockStat.st_dev == argStat.st_dev;
+                    } catch (ErrnoException e) {
+                        return false;
+                    }
+                };
+
+        verify(mMockNetd).ipSecSetEncapSocketOwner(argThat(fdMatcher), eq(Os.getuid()));
+        mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId);
+    }
+
+    @Test
+    public void testReserveNetId() {
+        final Range<Integer> netIdRange = ConnectivityManager.getIpSecNetIdRange();
+        for (int netId = netIdRange.getLower(); netId <= netIdRange.getUpper(); netId++) {
+            assertEquals(netId, mIpSecService.reserveNetId());
+        }
+
+        // Check that resource exhaustion triggers an exception
+        try {
+            mIpSecService.reserveNetId();
+            fail("Did not throw error for all netIds reserved");
+        } catch (IllegalStateException expected) {
+        }
+
+        // Now release one and try again
+        int releasedNetId =
+                netIdRange.getLower() + (netIdRange.getUpper() - netIdRange.getLower()) / 2;
+        mIpSecService.releaseNetId(releasedNetId);
+        assertEquals(releasedNetId, mIpSecService.reserveNetId());
+    }
+}
diff --git a/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt b/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt
new file mode 100644
index 0000000..5ec1119
--- /dev/null
+++ b/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt
@@ -0,0 +1,197 @@
+/*
+ * 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.
+ */
+
+// Don't warn about deprecated types anywhere in this test, because LegacyTypeTracker's very reason
+// for existence is to power deprecated APIs. The annotation has to apply to the whole file because
+// otherwise warnings will be generated by the imports of deprecated constants like TYPE_xxx.
+@file:Suppress("DEPRECATION")
+
+package com.android.server
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.content.pm.PackageManager.FEATURE_WIFI_DIRECT
+import android.net.ConnectivityManager.TYPE_ETHERNET
+import android.net.ConnectivityManager.TYPE_MOBILE
+import android.net.ConnectivityManager.TYPE_MOBILE_CBS
+import android.net.ConnectivityManager.TYPE_MOBILE_DUN
+import android.net.ConnectivityManager.TYPE_MOBILE_EMERGENCY
+import android.net.ConnectivityManager.TYPE_MOBILE_FOTA
+import android.net.ConnectivityManager.TYPE_MOBILE_HIPRI
+import android.net.ConnectivityManager.TYPE_MOBILE_IA
+import android.net.ConnectivityManager.TYPE_MOBILE_IMS
+import android.net.ConnectivityManager.TYPE_MOBILE_MMS
+import android.net.ConnectivityManager.TYPE_MOBILE_SUPL
+import android.net.ConnectivityManager.TYPE_VPN
+import android.net.ConnectivityManager.TYPE_WIFI
+import android.net.ConnectivityManager.TYPE_WIFI_P2P
+import android.net.ConnectivityManager.TYPE_WIMAX
+import android.net.EthernetManager
+import android.net.NetworkInfo.DetailedState.CONNECTED
+import android.net.NetworkInfo.DetailedState.DISCONNECTED
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.server.ConnectivityService.LegacyTypeTracker
+import com.android.server.connectivity.NetworkAgentInfo
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+
+const val UNSUPPORTED_TYPE = TYPE_WIMAX
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class LegacyTypeTrackerTest {
+    private val supportedTypes = arrayOf(TYPE_WIFI, TYPE_WIFI_P2P, TYPE_ETHERNET, TYPE_MOBILE,
+            TYPE_MOBILE_SUPL, TYPE_MOBILE_MMS, TYPE_MOBILE_SUPL, TYPE_MOBILE_DUN, TYPE_MOBILE_HIPRI,
+            TYPE_MOBILE_FOTA, TYPE_MOBILE_IMS, TYPE_MOBILE_CBS, TYPE_MOBILE_IA,
+            TYPE_MOBILE_EMERGENCY, TYPE_VPN)
+
+    private val mMockService = mock(ConnectivityService::class.java).apply {
+        doReturn(false).`when`(this).isDefaultNetwork(any())
+    }
+    private val mPm = mock(PackageManager::class.java)
+    private val mContext = mock(Context::class.java).apply {
+        doReturn(true).`when`(mPm).hasSystemFeature(FEATURE_WIFI)
+        doReturn(true).`when`(mPm).hasSystemFeature(FEATURE_WIFI_DIRECT)
+        doReturn(mPm).`when`(this).packageManager
+        doReturn(mock(EthernetManager::class.java)).`when`(this).getSystemService(
+                Context.ETHERNET_SERVICE)
+    }
+    private val mTm = mock(TelephonyManager::class.java).apply {
+        doReturn(true).`when`(this).isDataCapable
+    }
+
+    private fun makeTracker() = LegacyTypeTracker(mMockService).apply {
+        loadSupportedTypes(mContext, mTm)
+    }
+
+    @Test
+    fun testSupportedTypes() {
+        val tracker = makeTracker()
+        supportedTypes.forEach {
+            assertTrue(tracker.isTypeSupported(it))
+        }
+        assertFalse(tracker.isTypeSupported(UNSUPPORTED_TYPE))
+    }
+
+    @Test
+    fun testSupportedTypes_NoEthernet() {
+        doReturn(null).`when`(mContext).getSystemService(Context.ETHERNET_SERVICE)
+        assertFalse(makeTracker().isTypeSupported(TYPE_ETHERNET))
+    }
+
+    @Test
+    fun testSupportedTypes_NoTelephony() {
+        doReturn(false).`when`(mTm).isDataCapable
+        val tracker = makeTracker()
+        val nonMobileTypes = arrayOf(TYPE_WIFI, TYPE_WIFI_P2P, TYPE_ETHERNET, TYPE_VPN)
+        nonMobileTypes.forEach {
+            assertTrue(tracker.isTypeSupported(it))
+        }
+        supportedTypes.toSet().minus(nonMobileTypes).forEach {
+            assertFalse(tracker.isTypeSupported(it))
+        }
+    }
+
+    @Test
+    fun testSupportedTypes_NoWifiDirect() {
+        doReturn(false).`when`(mPm).hasSystemFeature(FEATURE_WIFI_DIRECT)
+        val tracker = makeTracker()
+        assertFalse(tracker.isTypeSupported(TYPE_WIFI_P2P))
+        supportedTypes.toSet().minus(TYPE_WIFI_P2P).forEach {
+            assertTrue(tracker.isTypeSupported(it))
+        }
+    }
+
+    @Test
+    fun testSupl() {
+        val tracker = makeTracker()
+        val mobileNai = mock(NetworkAgentInfo::class.java)
+        tracker.add(TYPE_MOBILE, mobileNai)
+        verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, CONNECTED, TYPE_MOBILE)
+        reset(mMockService)
+        tracker.add(TYPE_MOBILE_SUPL, mobileNai)
+        verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, CONNECTED, TYPE_MOBILE_SUPL)
+        reset(mMockService)
+        tracker.remove(TYPE_MOBILE_SUPL, mobileNai, false /* wasDefault */)
+        verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, DISCONNECTED, TYPE_MOBILE_SUPL)
+        reset(mMockService)
+        tracker.add(TYPE_MOBILE_SUPL, mobileNai)
+        verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, CONNECTED, TYPE_MOBILE_SUPL)
+        reset(mMockService)
+        tracker.remove(mobileNai, false)
+        verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, DISCONNECTED, TYPE_MOBILE_SUPL)
+        verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, DISCONNECTED, TYPE_MOBILE)
+    }
+
+    @Test
+    fun testAddNetwork() {
+        val tracker = makeTracker()
+        val mobileNai = mock(NetworkAgentInfo::class.java)
+        val wifiNai = mock(NetworkAgentInfo::class.java)
+        tracker.add(TYPE_MOBILE, mobileNai)
+        tracker.add(TYPE_WIFI, wifiNai)
+        assertSame(tracker.getNetworkForType(TYPE_MOBILE), mobileNai)
+        assertSame(tracker.getNetworkForType(TYPE_WIFI), wifiNai)
+        // Make sure adding a second NAI does not change the results.
+        val secondMobileNai = mock(NetworkAgentInfo::class.java)
+        tracker.add(TYPE_MOBILE, secondMobileNai)
+        assertSame(tracker.getNetworkForType(TYPE_MOBILE), mobileNai)
+        assertSame(tracker.getNetworkForType(TYPE_WIFI), wifiNai)
+        // Make sure removing a network that wasn't added for this type is a no-op.
+        tracker.remove(TYPE_MOBILE, wifiNai, false /* wasDefault */)
+        assertSame(tracker.getNetworkForType(TYPE_MOBILE), mobileNai)
+        assertSame(tracker.getNetworkForType(TYPE_WIFI), wifiNai)
+        // Remove the top network for mobile and make sure the second one becomes the network
+        // of record for this type.
+        tracker.remove(TYPE_MOBILE, mobileNai, false /* wasDefault */)
+        assertSame(tracker.getNetworkForType(TYPE_MOBILE), secondMobileNai)
+        assertSame(tracker.getNetworkForType(TYPE_WIFI), wifiNai)
+        // Make sure adding a network for an unsupported type does not register it.
+        tracker.add(UNSUPPORTED_TYPE, mobileNai)
+        assertNull(tracker.getNetworkForType(UNSUPPORTED_TYPE))
+    }
+
+    @Test
+    fun testBroadcastOnDisconnect() {
+        val tracker = makeTracker()
+        val mobileNai1 = mock(NetworkAgentInfo::class.java)
+        val mobileNai2 = mock(NetworkAgentInfo::class.java)
+        doReturn(false).`when`(mMockService).isDefaultNetwork(mobileNai1)
+        tracker.add(TYPE_MOBILE, mobileNai1)
+        verify(mMockService).sendLegacyNetworkBroadcast(mobileNai1, CONNECTED, TYPE_MOBILE)
+        reset(mMockService)
+        doReturn(false).`when`(mMockService).isDefaultNetwork(mobileNai2)
+        tracker.add(TYPE_MOBILE, mobileNai2)
+        verify(mMockService, never()).sendLegacyNetworkBroadcast(any(), any(), anyInt())
+        tracker.remove(TYPE_MOBILE, mobileNai1, false /* wasDefault */)
+        verify(mMockService).sendLegacyNetworkBroadcast(mobileNai1, DISCONNECTED, TYPE_MOBILE)
+        verify(mMockService).sendLegacyNetworkBroadcast(mobileNai2, CONNECTED, TYPE_MOBILE)
+    }
+}
diff --git a/tests/unit/java/com/android/server/NetIdManagerTest.kt b/tests/unit/java/com/android/server/NetIdManagerTest.kt
new file mode 100644
index 0000000..6f5e740
--- /dev/null
+++ b/tests/unit/java/com/android/server/NetIdManagerTest.kt
@@ -0,0 +1,53 @@
+/*
+ * 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 com.android.server
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.server.NetIdManager.MIN_NET_ID
+import com.android.testutils.assertThrows
+import com.android.testutils.ExceptionUtils.ThrowingRunnable
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NetIdManagerTest {
+    @Test
+    fun testReserveReleaseNetId() {
+        val manager = NetIdManager(MIN_NET_ID + 4)
+        assertEquals(MIN_NET_ID, manager.reserveNetId())
+        assertEquals(MIN_NET_ID + 1, manager.reserveNetId())
+        assertEquals(MIN_NET_ID + 2, manager.reserveNetId())
+        assertEquals(MIN_NET_ID + 3, manager.reserveNetId())
+
+        manager.releaseNetId(MIN_NET_ID + 1)
+        manager.releaseNetId(MIN_NET_ID + 3)
+        // IDs only loop once there is no higher ID available
+        assertEquals(MIN_NET_ID + 4, manager.reserveNetId())
+        assertEquals(MIN_NET_ID + 1, manager.reserveNetId())
+        assertEquals(MIN_NET_ID + 3, manager.reserveNetId())
+        assertThrows(IllegalStateException::class.java, ThrowingRunnable { manager.reserveNetId() })
+        manager.releaseNetId(MIN_NET_ID + 5)
+        // Still no ID available: MIN_NET_ID + 5 was not reserved
+        assertThrows(IllegalStateException::class.java, ThrowingRunnable { manager.reserveNetId() })
+        manager.releaseNetId(MIN_NET_ID + 2)
+        // Throwing an exception still leaves the manager in a working state
+        assertEquals(MIN_NET_ID + 2, manager.reserveNetId())
+    }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
new file mode 100644
index 0000000..13516d7
--- /dev/null
+++ b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
@@ -0,0 +1,315 @@
+/*
+ * 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;
+
+import static android.util.DebugUtils.valueToString;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.INetd;
+import android.net.INetdUnsolicitedEventListener;
+import android.net.LinkAddress;
+import android.net.NetworkPolicyManager;
+import android.os.BatteryStats;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.ArrayMap;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.app.IBatteryStats;
+import com.android.server.NetworkManagementService.Dependencies;
+import com.android.server.net.BaseNetworkObserver;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.BiFunction;
+
+/**
+ * Tests for {@link NetworkManagementService}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkManagementServiceTest {
+    private NetworkManagementService mNMService;
+    @Mock private Context mContext;
+    @Mock private IBatteryStats.Stub mBatteryStatsService;
+    @Mock private INetd.Stub mNetdService;
+
+    private static final int TEST_UID = 111;
+
+    @NonNull
+    @Captor
+    private ArgumentCaptor<INetdUnsolicitedEventListener> mUnsolListenerCaptor;
+
+    private final MockDependencies mDeps = new MockDependencies();
+
+    private final class MockDependencies extends Dependencies {
+        @Override
+        public IBinder getService(String name) {
+            switch (name) {
+                case BatteryStats.SERVICE_NAME:
+                    return mBatteryStatsService;
+                default:
+                    throw new UnsupportedOperationException("Unknown service " + name);
+            }
+        }
+
+        @Override
+        public void registerLocalService(NetworkManagementInternal nmi) {
+        }
+
+        @Override
+        public INetd getNetd() {
+            return mNetdService;
+        }
+
+        @Override
+        public int getCallingUid() {
+            return Process.SYSTEM_UID;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        doNothing().when(mNetdService)
+                .registerUnsolicitedEventListener(mUnsolListenerCaptor.capture());
+        // Start the service and wait until it connects to our socket.
+        mNMService = NetworkManagementService.create(mContext, mDeps);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mNMService.shutdown();
+    }
+
+    private static <T> T expectSoon(T mock) {
+        return verify(mock, timeout(200));
+    }
+
+    /**
+     * Tests that network observers work properly.
+     */
+    @Test
+    public void testNetworkObservers() throws Exception {
+        BaseNetworkObserver observer = mock(BaseNetworkObserver.class);
+        doReturn(new Binder()).when(observer).asBinder();  // Used by registerObserver.
+        mNMService.registerObserver(observer);
+
+        // Forget everything that happened to the mock so far, so we can explicitly verify
+        // everything that happens and does not happen to it from now on.
+
+        INetdUnsolicitedEventListener unsolListener = mUnsolListenerCaptor.getValue();
+        reset(observer);
+        // Now call unsolListener methods and ensure that the observer methods are
+        // called. After every method we expect a callback soon after; to ensure that
+        // invalid messages don't cause any callbacks, we call verifyNoMoreInteractions at the end.
+
+        /**
+         * Interface changes.
+         */
+        unsolListener.onInterfaceAdded("rmnet12");
+        expectSoon(observer).interfaceAdded("rmnet12");
+
+        unsolListener.onInterfaceRemoved("eth1");
+        expectSoon(observer).interfaceRemoved("eth1");
+
+        unsolListener.onInterfaceChanged("clat4", true);
+        expectSoon(observer).interfaceStatusChanged("clat4", true);
+
+        unsolListener.onInterfaceLinkStateChanged("rmnet0", false);
+        expectSoon(observer).interfaceLinkStateChanged("rmnet0", false);
+
+        /**
+         * Bandwidth control events.
+         */
+        unsolListener.onQuotaLimitReached("data", "rmnet_usb0");
+        expectSoon(observer).limitReached("data", "rmnet_usb0");
+
+        /**
+         * Interface class activity.
+         */
+        unsolListener.onInterfaceClassActivityChanged(true, 1, 1234, TEST_UID);
+        expectSoon(observer).interfaceClassDataActivityChanged(1, true, 1234, TEST_UID);
+
+        unsolListener.onInterfaceClassActivityChanged(false, 9, 5678, TEST_UID);
+        expectSoon(observer).interfaceClassDataActivityChanged(9, false, 5678, TEST_UID);
+
+        unsolListener.onInterfaceClassActivityChanged(false, 9, 4321, TEST_UID);
+        expectSoon(observer).interfaceClassDataActivityChanged(9, false, 4321, TEST_UID);
+
+        /**
+         * IP address changes.
+         */
+        unsolListener.onInterfaceAddressUpdated("fe80::1/64", "wlan0", 128, 253);
+        expectSoon(observer).addressUpdated("wlan0", new LinkAddress("fe80::1/64", 128, 253));
+
+        unsolListener.onInterfaceAddressRemoved("fe80::1/64", "wlan0", 128, 253);
+        expectSoon(observer).addressRemoved("wlan0", new LinkAddress("fe80::1/64", 128, 253));
+
+        unsolListener.onInterfaceAddressRemoved("2001:db8::1/64", "wlan0", 1, 0);
+        expectSoon(observer).addressRemoved("wlan0", new LinkAddress("2001:db8::1/64", 1, 0));
+
+        /**
+         * DNS information broadcasts.
+         */
+        unsolListener.onInterfaceDnsServerInfo("rmnet_usb0", 3600, new String[]{"2001:db8::1"});
+        expectSoon(observer).interfaceDnsServerInfo("rmnet_usb0", 3600,
+                new String[]{"2001:db8::1"});
+
+        unsolListener.onInterfaceDnsServerInfo("wlan0", 14400,
+                new String[]{"2001:db8::1", "2001:db8::2"});
+        expectSoon(observer).interfaceDnsServerInfo("wlan0", 14400,
+                new String[]{"2001:db8::1", "2001:db8::2"});
+
+        // We don't check for negative lifetimes, only for parse errors.
+        unsolListener.onInterfaceDnsServerInfo("wlan0", -3600, new String[]{"::1"});
+        expectSoon(observer).interfaceDnsServerInfo("wlan0", -3600,
+                new String[]{"::1"});
+
+        // No syntax checking on the addresses.
+        unsolListener.onInterfaceDnsServerInfo("wlan0", 600,
+                new String[]{"", "::", "", "foo", "::1"});
+        expectSoon(observer).interfaceDnsServerInfo("wlan0", 600,
+                new String[]{"", "::", "", "foo", "::1"});
+
+        // Make sure nothing else was called.
+        verifyNoMoreInteractions(observer);
+    }
+
+    @Test
+    public void testFirewallEnabled() {
+        mNMService.setFirewallEnabled(true);
+        assertTrue(mNMService.isFirewallEnabled());
+
+        mNMService.setFirewallEnabled(false);
+        assertFalse(mNMService.isFirewallEnabled());
+    }
+
+    @Test
+    public void testNetworkRestrictedDefault() {
+        assertFalse(mNMService.isNetworkRestricted(TEST_UID));
+    }
+
+    @Test
+    public void testMeteredNetworkRestrictions() throws RemoteException {
+        // Make sure the mocked netd method returns true.
+        doReturn(true).when(mNetdService).bandwidthEnableDataSaver(anyBoolean());
+
+        // Restrict usage of mobile data in background
+        mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, true);
+        assertTrue("Should be true since mobile data usage is restricted",
+                mNMService.isNetworkRestricted(TEST_UID));
+
+        mNMService.setDataSaverModeEnabled(true);
+        verify(mNetdService).bandwidthEnableDataSaver(true);
+
+        mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, false);
+        assertTrue("Should be true since data saver is on and the uid is not allowlisted",
+                mNMService.isNetworkRestricted(TEST_UID));
+
+        mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, true);
+        assertFalse("Should be false since data saver is on and the uid is allowlisted",
+                mNMService.isNetworkRestricted(TEST_UID));
+
+        // remove uid from allowlist and turn datasaver off again
+        mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false);
+        mNMService.setDataSaverModeEnabled(false);
+        verify(mNetdService).bandwidthEnableDataSaver(false);
+        assertFalse("Network should not be restricted when data saver is off",
+                mNMService.isNetworkRestricted(TEST_UID));
+    }
+
+    @Test
+    public void testFirewallChains() {
+        final ArrayMap<Integer, ArrayMap<Integer, Boolean>> expected = new ArrayMap<>();
+        // Dozable chain
+        final ArrayMap<Integer, Boolean> isRestrictedForDozable = new ArrayMap<>();
+        isRestrictedForDozable.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
+        isRestrictedForDozable.put(INetd.FIREWALL_RULE_ALLOW, false);
+        isRestrictedForDozable.put(INetd.FIREWALL_RULE_DENY, true);
+        expected.put(INetd.FIREWALL_CHAIN_DOZABLE, isRestrictedForDozable);
+        // Powersaver chain
+        final ArrayMap<Integer, Boolean> isRestrictedForPowerSave = new ArrayMap<>();
+        isRestrictedForPowerSave.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
+        isRestrictedForPowerSave.put(INetd.FIREWALL_RULE_ALLOW, false);
+        isRestrictedForPowerSave.put(INetd.FIREWALL_RULE_DENY, true);
+        expected.put(INetd.FIREWALL_CHAIN_POWERSAVE, isRestrictedForPowerSave);
+        // Standby chain
+        final ArrayMap<Integer, Boolean> isRestrictedForStandby = new ArrayMap<>();
+        isRestrictedForStandby.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, false);
+        isRestrictedForStandby.put(INetd.FIREWALL_RULE_ALLOW, false);
+        isRestrictedForStandby.put(INetd.FIREWALL_RULE_DENY, true);
+        expected.put(INetd.FIREWALL_CHAIN_STANDBY, isRestrictedForStandby);
+        // Restricted mode chain
+        final ArrayMap<Integer, Boolean> isRestrictedForRestrictedMode = new ArrayMap<>();
+        isRestrictedForRestrictedMode.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
+        isRestrictedForRestrictedMode.put(INetd.FIREWALL_RULE_ALLOW, false);
+        isRestrictedForRestrictedMode.put(INetd.FIREWALL_RULE_DENY, true);
+        expected.put(INetd.FIREWALL_CHAIN_RESTRICTED, isRestrictedForRestrictedMode);
+
+        final int[] chains = {
+                INetd.FIREWALL_CHAIN_STANDBY,
+                INetd.FIREWALL_CHAIN_POWERSAVE,
+                INetd.FIREWALL_CHAIN_DOZABLE,
+                INetd.FIREWALL_CHAIN_RESTRICTED
+        };
+        final int[] states = {
+                INetd.FIREWALL_RULE_ALLOW,
+                INetd.FIREWALL_RULE_DENY,
+                NetworkPolicyManager.FIREWALL_RULE_DEFAULT
+        };
+        BiFunction<Integer, Integer, String> errorMsg = (chain, state) -> {
+            return String.format("Unexpected value for chain: %s and state: %s",
+                    valueToString(INetd.class, "FIREWALL_CHAIN_", chain),
+                    valueToString(INetd.class, "FIREWALL_RULE_", state));
+        };
+        for (int chain : chains) {
+            final ArrayMap<Integer, Boolean> expectedValues = expected.get(chain);
+            mNMService.setFirewallChainEnabled(chain, true);
+            for (int state : states) {
+                mNMService.setFirewallUidRule(chain, TEST_UID, state);
+                assertEquals(errorMsg.apply(chain, state),
+                        expectedValues.get(state), mNMService.isNetworkRestricted(TEST_UID));
+            }
+            mNMService.setFirewallChainEnabled(chain, false);
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
new file mode 100644
index 0000000..20be5f4
--- /dev/null
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.any;
+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.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.NsdService.DaemonConnection;
+import com.android.server.NsdService.DaemonConnectionSupplier;
+import com.android.server.NsdService.NativeCallbackReceiver;
+
+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;
+
+// TODOs:
+//  - test client can send requests and receive replies
+//  - test NSD_ON ENABLE/DISABLED listening
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NsdServiceTest {
+
+    static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
+    private static final long CLEANUP_DELAY_MS = 500;
+
+    long mTimeoutMs = 100; // non-final so that tests can adjust the value.
+
+    @Mock Context mContext;
+    @Mock ContentResolver mResolver;
+    @Mock NsdService.NsdSettings mSettings;
+    @Mock DaemonConnection mDaemon;
+    NativeCallbackReceiver mDaemonCallback;
+    HandlerThread mThread;
+    TestHandler mHandler;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mThread = new HandlerThread("mock-service-handler");
+        mThread.start();
+        mHandler = new TestHandler(mThread.getLooper());
+        when(mContext.getContentResolver()).thenReturn(mResolver);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mThread != null) {
+            mThread.quit();
+            mThread = null;
+        }
+    }
+
+    @Test
+    public void testNoDaemonStartedWhenClientsConnect() {
+        when(mSettings.isEnabled()).thenReturn(true);
+
+        NsdService service = makeService();
+
+        // Creating an NsdManager will not cause any cmds executed, which means
+        // no daemon is started.
+        NsdManager client1 = connectClient(service);
+        verify(mDaemon, never()).execute(any());
+
+        // Creating another NsdManager will not cause any cmds executed.
+        NsdManager client2 = connectClient(service);
+        verify(mDaemon, never()).execute(any());
+
+        client1.disconnect();
+        // Still 1 client remains, daemon shouldn't be stopped.
+        verify(mDaemon, never()).maybeStop();
+
+        client2.disconnect();
+        // All clients are disconnected, stop the daemon after CLEANUP_DELAY_MS.
+        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
+
+        client1.disconnect();
+        client2.disconnect();
+    }
+
+    @Test
+    public void testClientRequestsAreGCedAtDisconnection() {
+        when(mSettings.isEnabled()).thenReturn(true);
+        when(mDaemon.execute(any())).thenReturn(true);
+
+        NsdService service = makeService();
+        NsdManager client = connectClient(service);
+
+        verify(mDaemon, never()).maybeStart();
+        verify(mDaemon, never()).execute(any());
+
+        NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
+        request.setPort(2201);
+
+        // Client registration request
+        NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
+        client.registerService(request, PROTOCOL, listener1);
+        verify(mDaemon, timeout(mTimeoutMs).times(1)).maybeStart();
+        verifyDaemonCommand("register 2 a_name a_type 2201");
+
+        // Client discovery request
+        NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class);
+        client.discoverServices("a_type", PROTOCOL, listener2);
+        verify(mDaemon, timeout(mTimeoutMs).times(1)).maybeStart();
+        verifyDaemonCommand("discover 3 a_type");
+
+        // Client resolve request
+        NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
+        client.resolveService(request, listener3);
+        verify(mDaemon, timeout(mTimeoutMs).times(1)).maybeStart();
+        verifyDaemonCommand("resolve 4 a_name a_type local.");
+
+        // Client disconnects, stop the daemon after CLEANUP_DELAY_MS.
+        client.disconnect();
+        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
+
+        // checks that request are cleaned
+        verifyDaemonCommands("stop-register 2", "stop-discover 3", "stop-resolve 4");
+
+        client.disconnect();
+    }
+
+    @Test
+    public void testCleanupDelayNoRequestActive() {
+        when(mSettings.isEnabled()).thenReturn(true);
+        when(mDaemon.execute(any())).thenReturn(true);
+
+        NsdService service = makeService();
+        NsdManager client = connectClient(service);
+
+        NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type");
+        request.setPort(2201);
+        NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class);
+        client.registerService(request, PROTOCOL, listener1);
+        verify(mDaemon, timeout(mTimeoutMs).times(1)).maybeStart();
+        verifyDaemonCommand("register 2 a_name a_type 2201");
+
+        client.unregisterService(listener1);
+        verifyDaemonCommand("stop-register 2");
+
+        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
+        reset(mDaemon);
+        client.disconnect();
+        // Client disconnects, after CLEANUP_DELAY_MS, maybeStop the daemon.
+        verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS);
+    }
+
+    NsdService makeService() {
+        DaemonConnectionSupplier supplier = (callback) -> {
+            mDaemonCallback = callback;
+            return mDaemon;
+        };
+        NsdService service = new NsdService(mContext, mSettings,
+                mHandler, supplier, CLEANUP_DELAY_MS);
+        verify(mDaemon, never()).execute(any(String.class));
+        return service;
+    }
+
+    NsdManager connectClient(NsdService service) {
+        return new NsdManager(mContext, service);
+    }
+
+    void verifyDelayMaybeStopDaemon(long cleanupDelayMs) {
+        // Stop daemon shouldn't be called immediately.
+        verify(mDaemon, timeout(mTimeoutMs).times(0)).maybeStop();
+        // Clean up the daemon after CLEANUP_DELAY_MS.
+        verify(mDaemon, timeout(cleanupDelayMs + mTimeoutMs)).maybeStop();
+    }
+
+    void verifyDaemonCommands(String... wants) {
+        verifyDaemonCommand(String.join(" ", wants), wants.length);
+    }
+
+    void verifyDaemonCommand(String want) {
+        verifyDaemonCommand(want, 1);
+    }
+
+    void verifyDaemonCommand(String want, int n) {
+        ArgumentCaptor<Object> argumentsCaptor = ArgumentCaptor.forClass(Object.class);
+        verify(mDaemon, timeout(mTimeoutMs).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);
+        when(mDaemon.execute(any())).thenReturn(true);
+    }
+
+    public static class TestHandler extends Handler {
+        public Message lastMessage;
+
+        TestHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            lastMessage = obtainMessage();
+            lastMessage.copyFrom(msg);
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
new file mode 100644
index 0000000..0ffeec9
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -0,0 +1,433 @@
+/*
+ * 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.connectivity;
+
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_DEFAULT_MODE;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_SPECIFIER;
+import static android.net.NetworkCapabilities.MAX_TRANSPORT;
+import static android.net.NetworkCapabilities.MIN_TRANSPORT;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE;
+import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS;
+
+import static com.android.testutils.MiscAsserts.assertContainsExactly;
+import static com.android.testutils.MiscAsserts.assertContainsStringsExactly;
+import static com.android.testutils.MiscAsserts.assertFieldCountEquals;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.ConnectivitySettingsManager;
+import android.net.IDnsResolver;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.ResolverOptionsParcel;
+import android.net.ResolverParamsParcel;
+import android.net.RouteInfo;
+import android.net.shared.PrivateDnsConfig;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
+import android.util.SparseArray;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.MessageUtils;
+import com.android.internal.util.test.FakeSettingsProvider;
+
+import libcore.net.InetAddressUtils;
+
+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.Arrays;
+
+/**
+ * Tests for {@link DnsManager}.
+ *
+ * Build, install and run with:
+ *  runtest frameworks-net -c com.android.server.connectivity.DnsManagerTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DnsManagerTest {
+    static final String TEST_IFACENAME = "test_wlan0";
+    static final int TEST_NETID = 100;
+    static final int TEST_NETID_ALTERNATE = 101;
+    static final int TEST_NETID_UNTRACKED = 102;
+    static final int TEST_DEFAULT_SAMPLE_VALIDITY_SECONDS = 1800;
+    static final int TEST_DEFAULT_SUCCESS_THRESHOLD_PERCENT = 25;
+    static final int TEST_DEFAULT_MIN_SAMPLES = 8;
+    static final int TEST_DEFAULT_MAX_SAMPLES = 64;
+    static final int[] TEST_TRANSPORT_TYPES = {TRANSPORT_WIFI, TRANSPORT_VPN};
+
+    DnsManager mDnsManager;
+    MockContentResolver mContentResolver;
+
+    @Mock Context mCtx;
+    @Mock IDnsResolver mMockDnsResolver;
+
+    private void assertResolverOptionsEquals(
+            @NonNull ResolverOptionsParcel actual,
+            @NonNull ResolverOptionsParcel expected) {
+        assertEquals(actual.hosts, expected.hosts);
+        assertEquals(actual.tcMode, expected.tcMode);
+        assertEquals(actual.enforceDnsUid, expected.enforceDnsUid);
+        assertFieldCountEquals(3, ResolverOptionsParcel.class);
+    }
+
+    private void assertResolverParamsEquals(@NonNull ResolverParamsParcel actual,
+            @NonNull ResolverParamsParcel expected) {
+        assertEquals(actual.netId, expected.netId);
+        assertEquals(actual.sampleValiditySeconds, expected.sampleValiditySeconds);
+        assertEquals(actual.successThreshold, expected.successThreshold);
+        assertEquals(actual.minSamples, expected.minSamples);
+        assertEquals(actual.maxSamples, expected.maxSamples);
+        assertEquals(actual.baseTimeoutMsec, expected.baseTimeoutMsec);
+        assertEquals(actual.retryCount, expected.retryCount);
+        assertContainsStringsExactly(actual.servers, expected.servers);
+        assertContainsStringsExactly(actual.domains, expected.domains);
+        assertEquals(actual.tlsName, expected.tlsName);
+        assertContainsStringsExactly(actual.tlsServers, expected.tlsServers);
+        assertContainsStringsExactly(actual.tlsFingerprints, expected.tlsFingerprints);
+        assertEquals(actual.caCertificate, expected.caCertificate);
+        assertEquals(actual.tlsConnectTimeoutMs, expected.tlsConnectTimeoutMs);
+        assertResolverOptionsEquals(actual.resolverOptions, expected.resolverOptions);
+        assertContainsExactly(actual.transportTypes, expected.transportTypes);
+        assertFieldCountEquals(16, ResolverParamsParcel.class);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mContentResolver = new MockContentResolver();
+        mContentResolver.addProvider(Settings.AUTHORITY,
+                new FakeSettingsProvider());
+        when(mCtx.getContentResolver()).thenReturn(mContentResolver);
+        mDnsManager = new DnsManager(mCtx, mMockDnsResolver);
+
+        // Clear the private DNS settings
+        Settings.Global.putString(mContentResolver, PRIVATE_DNS_DEFAULT_MODE, "");
+        Settings.Global.putString(mContentResolver, PRIVATE_DNS_MODE, "");
+        Settings.Global.putString(mContentResolver, PRIVATE_DNS_SPECIFIER, "");
+    }
+
+    @Test
+    public void testTrackedValidationUpdates() throws Exception {
+        mDnsManager.updatePrivateDns(new Network(TEST_NETID),
+                mDnsManager.getPrivateDnsConfig());
+        mDnsManager.updatePrivateDns(new Network(TEST_NETID_ALTERNATE),
+                mDnsManager.getPrivateDnsConfig());
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(TEST_IFACENAME);
+        lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
+        lp.addDnsServer(InetAddress.getByName("4.4.4.4"));
+
+        // Send a validation event that is tracked on the alternate netId
+        mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+        mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
+        mDnsManager.flushVmDnsCache();
+        mDnsManager.updateTransportsForNetwork(TEST_NETID_ALTERNATE, TEST_TRANSPORT_TYPES);
+        mDnsManager.noteDnsServersForNetwork(TEST_NETID_ALTERNATE, lp);
+        mDnsManager.flushVmDnsCache();
+        mDnsManager.updatePrivateDnsValidation(
+                new DnsManager.PrivateDnsValidationUpdate(TEST_NETID_ALTERNATE,
+                        InetAddress.parseNumericAddress("4.4.4.4"), "",
+                        VALIDATION_RESULT_SUCCESS));
+        LinkProperties fixedLp = new LinkProperties(lp);
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, fixedLp);
+        assertFalse(fixedLp.isPrivateDnsActive());
+        assertNull(fixedLp.getPrivateDnsServerName());
+        fixedLp = new LinkProperties(lp);
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID_ALTERNATE, fixedLp);
+        assertTrue(fixedLp.isPrivateDnsActive());
+        assertNull(fixedLp.getPrivateDnsServerName());
+        assertEquals(Arrays.asList(InetAddress.getByName("4.4.4.4")),
+                fixedLp.getValidatedPrivateDnsServers());
+
+        // Set up addresses for strict mode and switch to it.
+        lp.addLinkAddress(new LinkAddress("192.0.2.4/24"));
+        lp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("192.0.2.4"),
+                TEST_IFACENAME));
+        lp.addLinkAddress(new LinkAddress("2001:db8:1::1/64"));
+        lp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("2001:db8:1::1"),
+                TEST_IFACENAME));
+
+        ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
+        ConnectivitySettingsManager.setPrivateDnsHostname(mCtx, "strictmode.com");
+        mDnsManager.updatePrivateDns(new Network(TEST_NETID),
+                new PrivateDnsConfig("strictmode.com", new InetAddress[] {
+                    InetAddress.parseNumericAddress("6.6.6.6"),
+                    InetAddress.parseNumericAddress("2001:db8:66:66::1")
+                    }));
+        mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+        mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
+        mDnsManager.flushVmDnsCache();
+        fixedLp = new LinkProperties(lp);
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, fixedLp);
+        assertTrue(fixedLp.isPrivateDnsActive());
+        assertEquals("strictmode.com", fixedLp.getPrivateDnsServerName());
+        // No validation events yet.
+        assertEquals(Arrays.asList(new InetAddress[0]), fixedLp.getValidatedPrivateDnsServers());
+        // Validate one.
+        mDnsManager.updatePrivateDnsValidation(
+                new DnsManager.PrivateDnsValidationUpdate(TEST_NETID,
+                        InetAddress.parseNumericAddress("6.6.6.6"), "strictmode.com",
+                        VALIDATION_RESULT_SUCCESS));
+        fixedLp = new LinkProperties(lp);
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, fixedLp);
+        assertEquals(Arrays.asList(InetAddress.parseNumericAddress("6.6.6.6")),
+                fixedLp.getValidatedPrivateDnsServers());
+        // Validate the 2nd one.
+        mDnsManager.updatePrivateDnsValidation(
+                new DnsManager.PrivateDnsValidationUpdate(TEST_NETID,
+                        InetAddress.parseNumericAddress("2001:db8:66:66::1"), "strictmode.com",
+                        VALIDATION_RESULT_SUCCESS));
+        fixedLp = new LinkProperties(lp);
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, fixedLp);
+        assertEquals(Arrays.asList(
+                        InetAddress.parseNumericAddress("2001:db8:66:66::1"),
+                        InetAddress.parseNumericAddress("6.6.6.6")),
+                fixedLp.getValidatedPrivateDnsServers());
+    }
+
+    @Test
+    public void testIgnoreUntrackedValidationUpdates() throws Exception {
+        // The PrivateDnsConfig map is empty, so no validation events will
+        // be tracked.
+        LinkProperties lp = new LinkProperties();
+        lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
+        mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+        mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
+        mDnsManager.flushVmDnsCache();
+        mDnsManager.updatePrivateDnsValidation(
+                new DnsManager.PrivateDnsValidationUpdate(TEST_NETID,
+                        InetAddress.parseNumericAddress("3.3.3.3"), "",
+                        VALIDATION_RESULT_SUCCESS));
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
+        assertFalse(lp.isPrivateDnsActive());
+        assertNull(lp.getPrivateDnsServerName());
+
+        // Validation event has untracked netId
+        mDnsManager.updatePrivateDns(new Network(TEST_NETID),
+                mDnsManager.getPrivateDnsConfig());
+        mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+        mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
+        mDnsManager.flushVmDnsCache();
+        mDnsManager.updatePrivateDnsValidation(
+                new DnsManager.PrivateDnsValidationUpdate(TEST_NETID_UNTRACKED,
+                        InetAddress.parseNumericAddress("3.3.3.3"), "",
+                        VALIDATION_RESULT_SUCCESS));
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
+        assertFalse(lp.isPrivateDnsActive());
+        assertNull(lp.getPrivateDnsServerName());
+
+        // Validation event has untracked ipAddress
+        mDnsManager.updatePrivateDnsValidation(
+                new DnsManager.PrivateDnsValidationUpdate(TEST_NETID,
+                        InetAddress.parseNumericAddress("4.4.4.4"), "",
+                        VALIDATION_RESULT_SUCCESS));
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
+        assertFalse(lp.isPrivateDnsActive());
+        assertNull(lp.getPrivateDnsServerName());
+
+        // Validation event has untracked hostname
+        mDnsManager.updatePrivateDnsValidation(
+                new DnsManager.PrivateDnsValidationUpdate(TEST_NETID,
+                        InetAddress.parseNumericAddress("3.3.3.3"), "hostname",
+                        VALIDATION_RESULT_SUCCESS));
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
+        assertFalse(lp.isPrivateDnsActive());
+        assertNull(lp.getPrivateDnsServerName());
+
+        // Validation event failed
+        mDnsManager.updatePrivateDnsValidation(
+                new DnsManager.PrivateDnsValidationUpdate(TEST_NETID,
+                        InetAddress.parseNumericAddress("3.3.3.3"), "",
+                        VALIDATION_RESULT_FAILURE));
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
+        assertFalse(lp.isPrivateDnsActive());
+        assertNull(lp.getPrivateDnsServerName());
+
+        // Network removed
+        mDnsManager.removeNetwork(new Network(TEST_NETID));
+        mDnsManager.updatePrivateDnsValidation(
+                new DnsManager.PrivateDnsValidationUpdate(TEST_NETID,
+                        InetAddress.parseNumericAddress("3.3.3.3"), "", VALIDATION_RESULT_SUCCESS));
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
+        assertFalse(lp.isPrivateDnsActive());
+        assertNull(lp.getPrivateDnsServerName());
+
+        // Turn private DNS mode off
+        ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_OFF);
+        mDnsManager.updatePrivateDns(new Network(TEST_NETID),
+                mDnsManager.getPrivateDnsConfig());
+        mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+        mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
+        mDnsManager.flushVmDnsCache();
+        mDnsManager.updatePrivateDnsValidation(
+                new DnsManager.PrivateDnsValidationUpdate(TEST_NETID,
+                        InetAddress.parseNumericAddress("3.3.3.3"), "",
+                        VALIDATION_RESULT_SUCCESS));
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
+        assertFalse(lp.isPrivateDnsActive());
+        assertNull(lp.getPrivateDnsServerName());
+    }
+
+    @Test
+    public void testOverrideDefaultMode() throws Exception {
+        // Hard-coded default is opportunistic mode.
+        final PrivateDnsConfig cfgAuto = DnsManager.getPrivateDnsConfig(mCtx);
+        assertTrue(cfgAuto.useTls);
+        assertEquals("", cfgAuto.hostname);
+        assertEquals(new InetAddress[0], cfgAuto.ips);
+
+        // Pretend a gservices push sets the default to "off".
+        ConnectivitySettingsManager.setPrivateDnsDefaultMode(mCtx, PRIVATE_DNS_MODE_OFF);
+        final PrivateDnsConfig cfgOff = DnsManager.getPrivateDnsConfig(mCtx);
+        assertFalse(cfgOff.useTls);
+        assertEquals("", cfgOff.hostname);
+        assertEquals(new InetAddress[0], cfgOff.ips);
+
+        // Strict mode still works.
+        ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
+        ConnectivitySettingsManager.setPrivateDnsHostname(mCtx, "strictmode.com");
+        final PrivateDnsConfig cfgStrict = DnsManager.getPrivateDnsConfig(mCtx);
+        assertTrue(cfgStrict.useTls);
+        assertEquals("strictmode.com", cfgStrict.hostname);
+        assertEquals(new InetAddress[0], cfgStrict.ips);
+    }
+
+    @Test
+    public void testSendDnsConfiguration() throws Exception {
+        reset(mMockDnsResolver);
+        mDnsManager.updatePrivateDns(new Network(TEST_NETID),
+                mDnsManager.getPrivateDnsConfig());
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(TEST_IFACENAME);
+        lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
+        lp.addDnsServer(InetAddress.getByName("4.4.4.4"));
+        mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+        mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
+        mDnsManager.flushVmDnsCache();
+
+        final ArgumentCaptor<ResolverParamsParcel> resolverParamsParcelCaptor =
+                ArgumentCaptor.forClass(ResolverParamsParcel.class);
+        verify(mMockDnsResolver, times(1)).setResolverConfiguration(
+                resolverParamsParcelCaptor.capture());
+        final ResolverParamsParcel actualParams = resolverParamsParcelCaptor.getValue();
+        final ResolverParamsParcel expectedParams = new ResolverParamsParcel();
+        expectedParams.netId = TEST_NETID;
+        expectedParams.sampleValiditySeconds = TEST_DEFAULT_SAMPLE_VALIDITY_SECONDS;
+        expectedParams.successThreshold = TEST_DEFAULT_SUCCESS_THRESHOLD_PERCENT;
+        expectedParams.minSamples = TEST_DEFAULT_MIN_SAMPLES;
+        expectedParams.maxSamples = TEST_DEFAULT_MAX_SAMPLES;
+        expectedParams.servers = new String[]{"3.3.3.3", "4.4.4.4"};
+        expectedParams.domains = new String[]{};
+        expectedParams.tlsName = "";
+        expectedParams.tlsServers = new String[]{"3.3.3.3", "4.4.4.4"};
+        expectedParams.transportTypes = TEST_TRANSPORT_TYPES;
+        expectedParams.resolverOptions = new ResolverOptionsParcel();
+        assertResolverParamsEquals(actualParams, expectedParams);
+    }
+
+    @Test
+    public void testTransportTypesEqual() throws Exception {
+        SparseArray<String> ncTransTypes = MessageUtils.findMessageNames(
+                new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" });
+        SparseArray<String> dnsTransTypes = MessageUtils.findMessageNames(
+                new Class[] { IDnsResolver.class }, new String[]{ "TRANSPORT_" });
+        assertEquals(0, MIN_TRANSPORT);
+        assertEquals(MAX_TRANSPORT + 1, ncTransTypes.size());
+        // TRANSPORT_UNKNOWN in IDnsResolver is defined to -1 and only for resolver.
+        assertEquals("TRANSPORT_UNKNOWN", dnsTransTypes.get(-1));
+        assertEquals(ncTransTypes.size(), dnsTransTypes.size() - 1);
+        for (int i = MIN_TRANSPORT; i < MAX_TRANSPORT; i++) {
+            String name = ncTransTypes.get(i, null);
+            assertNotNull("Could not find NetworkCapabilies.TRANSPORT_* constant equal to "
+                    + i, name);
+            assertEquals(name, dnsTransTypes.get(i));
+        }
+    }
+
+    @Test
+    public void testGetPrivateDnsConfigForNetwork() throws Exception {
+        final Network network = new Network(TEST_NETID);
+        final InetAddress dnsAddr = InetAddressUtils.parseNumericAddress("3.3.3.3");
+        final InetAddress[] tlsAddrs = new InetAddress[]{
+            InetAddressUtils.parseNumericAddress("6.6.6.6"),
+            InetAddressUtils.parseNumericAddress("2001:db8:66:66::1")
+        };
+        final String tlsName = "strictmode.com";
+        LinkProperties lp = new LinkProperties();
+        lp.addDnsServer(dnsAddr);
+
+        // The PrivateDnsConfig map is empty, so the default PRIVATE_DNS_OFF is returned.
+        PrivateDnsConfig privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
+        assertFalse(privateDnsCfg.useTls);
+        assertEquals("", privateDnsCfg.hostname);
+        assertEquals(new InetAddress[0], privateDnsCfg.ips);
+
+        // An entry with default PrivateDnsConfig is added to the PrivateDnsConfig map.
+        mDnsManager.updatePrivateDns(network, mDnsManager.getPrivateDnsConfig());
+        mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
+        mDnsManager.updatePrivateDnsValidation(
+                new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, dnsAddr, "",
+                        VALIDATION_RESULT_SUCCESS));
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
+        privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
+        assertTrue(privateDnsCfg.useTls);
+        assertEquals("", privateDnsCfg.hostname);
+        assertEquals(new InetAddress[0], privateDnsCfg.ips);
+
+        // The original entry is overwritten by a new PrivateDnsConfig.
+        mDnsManager.updatePrivateDns(network, new PrivateDnsConfig(tlsName, tlsAddrs));
+        mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
+        privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
+        assertTrue(privateDnsCfg.useTls);
+        assertEquals(tlsName, privateDnsCfg.hostname);
+        assertEquals(tlsAddrs, privateDnsCfg.ips);
+
+        // The network is removed, so the PrivateDnsConfig map becomes empty again.
+        mDnsManager.removeNetwork(network);
+        privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
+        assertFalse(privateDnsCfg.useTls);
+        assertEquals("", privateDnsCfg.hostname);
+        assertEquals(new InetAddress[0], privateDnsCfg.ips);
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
new file mode 100644
index 0000000..45b575a
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
@@ -0,0 +1,135 @@
+/*
+ * 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 android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkScore.KEEP_CONNECTED_NONE
+import android.text.TextUtils
+import android.util.ArraySet
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.server.connectivity.FullScore.MAX_CS_MANAGED_POLICY
+import com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED
+import com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED
+import com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED
+import com.android.server.connectivity.FullScore.POLICY_IS_VPN
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.collections.minOfOrNull
+import kotlin.collections.maxOfOrNull
+import kotlin.reflect.full.staticProperties
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class FullScoreTest {
+    // Convenience methods
+    fun FullScore.withPolicies(
+        validated: Boolean = false,
+        vpn: Boolean = false,
+        onceChosen: Boolean = false,
+        acceptUnvalidated: Boolean = false
+    ): FullScore {
+        val nac = NetworkAgentConfig.Builder().apply {
+            setUnvalidatedConnectivityAcceptable(acceptUnvalidated)
+            setExplicitlySelected(onceChosen)
+        }.build()
+        val nc = NetworkCapabilities.Builder().apply {
+            if (vpn) addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+            if (validated) addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+        }.build()
+        return mixInScore(nc, nac, validated, false /* yieldToBadWifi */)
+    }
+
+    @Test
+    fun testGetLegacyInt() {
+        val ns = FullScore(50, 0L /* policy */, KEEP_CONNECTED_NONE)
+        assertEquals(10, ns.legacyInt) // -40 penalty for not being validated
+        assertEquals(50, ns.legacyIntAsValidated)
+
+        val vpnNs = FullScore(101, 0L /* policy */, KEEP_CONNECTED_NONE).withPolicies(vpn = true)
+        assertEquals(101, vpnNs.legacyInt) // VPNs are not subject to unvalidation penalty
+        assertEquals(101, vpnNs.legacyIntAsValidated)
+        assertEquals(101, vpnNs.withPolicies(validated = true).legacyInt)
+        assertEquals(101, vpnNs.withPolicies(validated = true).legacyIntAsValidated)
+
+        val validatedNs = ns.withPolicies(validated = true)
+        assertEquals(50, validatedNs.legacyInt) // No penalty, this is validated
+        assertEquals(50, validatedNs.legacyIntAsValidated)
+
+        val chosenNs = ns.withPolicies(onceChosen = true)
+        assertEquals(10, chosenNs.legacyInt)
+        assertEquals(100, chosenNs.legacyIntAsValidated)
+        assertEquals(10, chosenNs.withPolicies(acceptUnvalidated = true).legacyInt)
+        assertEquals(50, chosenNs.withPolicies(acceptUnvalidated = true).legacyIntAsValidated)
+    }
+
+    @Test
+    fun testToString() {
+        val string = FullScore(10, 0L /* policy */, KEEP_CONNECTED_NONE)
+                .withPolicies(vpn = true, acceptUnvalidated = true).toString()
+        assertTrue(string.contains("Score(10"), string)
+        assertTrue(string.contains("ACCEPT_UNVALIDATED"), string)
+        assertTrue(string.contains("IS_VPN"), string)
+        assertFalse(string.contains("IS_VALIDATED"), string)
+        val foundNames = ArraySet<String>()
+        getAllPolicies().forEach {
+            val name = FullScore.policyNameOf(it.get() as Int)
+            assertFalse(TextUtils.isEmpty(name))
+            assertFalse(foundNames.contains(name))
+            foundNames.add(name)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            FullScore.policyNameOf(MAX_CS_MANAGED_POLICY + 1)
+        }
+    }
+
+    fun getAllPolicies() = Regex("POLICY_.*").let { nameRegex ->
+        FullScore::class.staticProperties.filter { it.name.matches(nameRegex) }
+    }
+
+    @Test
+    fun testHasPolicy() {
+        val ns = FullScore(50, 0L /* policy */, KEEP_CONNECTED_NONE)
+        assertFalse(ns.hasPolicy(POLICY_IS_VALIDATED))
+        assertFalse(ns.hasPolicy(POLICY_IS_VPN))
+        assertFalse(ns.hasPolicy(POLICY_EVER_USER_SELECTED))
+        assertFalse(ns.hasPolicy(POLICY_ACCEPT_UNVALIDATED))
+        assertTrue(ns.withPolicies(validated = true).hasPolicy(POLICY_IS_VALIDATED))
+        assertTrue(ns.withPolicies(vpn = true).hasPolicy(POLICY_IS_VPN))
+        assertTrue(ns.withPolicies(onceChosen = true).hasPolicy(POLICY_EVER_USER_SELECTED))
+        assertTrue(ns.withPolicies(acceptUnvalidated = true).hasPolicy(POLICY_ACCEPT_UNVALIDATED))
+    }
+
+    @Test
+    fun testMinMaxPolicyConstants() {
+        val policies = getAllPolicies()
+
+        policies.forEach { policy ->
+            assertTrue(policy.get() as Int >= FullScore.MIN_CS_MANAGED_POLICY)
+            assertTrue(policy.get() as Int <= FullScore.MAX_CS_MANAGED_POLICY)
+        }
+        assertEquals(FullScore.MIN_CS_MANAGED_POLICY,
+                policies.minOfOrNull { it.get() as Int })
+        assertEquals(FullScore.MAX_CS_MANAGED_POLICY,
+                policies.maxOfOrNull { it.get() as Int })
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
new file mode 100644
index 0000000..70495cc
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
@@ -0,0 +1,561 @@
+/*
+ * 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.connectivity;
+
+import static com.android.server.connectivity.MetricsTestUtil.aLong;
+import static com.android.server.connectivity.MetricsTestUtil.aString;
+import static com.android.server.connectivity.MetricsTestUtil.aType;
+import static com.android.server.connectivity.MetricsTestUtil.anInt;
+import static com.android.server.connectivity.MetricsTestUtil.describeIpEvent;
+import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.BLUETOOTH;
+import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.CELLULAR;
+import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog;
+import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.MULTIPLE;
+import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.WIFI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.net.ConnectivityMetricsEvent;
+import android.net.metrics.ApfProgramEvent;
+import android.net.metrics.ApfStats;
+import android.net.metrics.DefaultNetworkEvent;
+import android.net.metrics.DhcpClientEvent;
+import android.net.metrics.DhcpErrorEvent;
+import android.net.metrics.IpManagerEvent;
+import android.net.metrics.IpReachabilityEvent;
+import android.net.metrics.NetworkEvent;
+import android.net.metrics.RaEvent;
+import android.net.metrics.ValidationProbeEvent;
+import android.net.metrics.WakeupStats;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+// TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto.
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpConnectivityEventBuilderTest {
+
+    @Test
+    public void testLinkLayerInferrence() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(IpReachabilityEvent.class),
+                anInt(IpReachabilityEvent.NUD_FAILED));
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 0",
+                "  network_id: 0",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  ip_reachability_event <",
+                "    event_type: 512",
+                "    if_name: \"\"",
+                "  >",
+                ">",
+                "version: 2\n");
+        verifySerialization(want, ev);
+
+        ev.netId = 123;
+        ev.transports = 3; // transports have priority for inferrence of link layer
+        ev.ifname = "wlan0";
+        want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                String.format("  link_layer: %d", MULTIPLE),
+                "  network_id: 123",
+                "  time_ms: 1",
+                "  transports: 3",
+                "  ip_reachability_event <",
+                "    event_type: 512",
+                "    if_name: \"\"",
+                "  >",
+                ">",
+                "version: 2\n");
+        verifySerialization(want, ev);
+
+        ev.transports = 1;
+        ev.ifname = null;
+        want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                String.format("  link_layer: %d", CELLULAR),
+                "  network_id: 123",
+                "  time_ms: 1",
+                "  transports: 1",
+                "  ip_reachability_event <",
+                "    event_type: 512",
+                "    if_name: \"\"",
+                "  >",
+                ">",
+                "version: 2\n");
+        verifySerialization(want, ev);
+
+        ev.transports = 0;
+        ev.ifname = "not_inferred";
+        want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"not_inferred\"",
+                "  link_layer: 0",
+                "  network_id: 123",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  ip_reachability_event <",
+                "    event_type: 512",
+                "    if_name: \"\"",
+                "  >",
+                ">",
+                "version: 2\n");
+        verifySerialization(want, ev);
+
+        ev.ifname = "bt-pan";
+        want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                String.format("  link_layer: %d", BLUETOOTH),
+                "  network_id: 123",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  ip_reachability_event <",
+                "    event_type: 512",
+                "    if_name: \"\"",
+                "  >",
+                ">",
+                "version: 2\n");
+        verifySerialization(want, ev);
+
+        ev.ifname = "rmnet_ipa0";
+        want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                String.format("  link_layer: %d", CELLULAR),
+                "  network_id: 123",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  ip_reachability_event <",
+                "    event_type: 512",
+                "    if_name: \"\"",
+                "  >",
+                ">",
+                "version: 2\n");
+        verifySerialization(want, ev);
+
+        ev.ifname = "wlan0";
+        want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                String.format("  link_layer: %d", WIFI),
+                "  network_id: 123",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  ip_reachability_event <",
+                "    event_type: 512",
+                "    if_name: \"\"",
+                "  >",
+                ">",
+                "version: 2\n");
+        verifySerialization(want, ev);
+    }
+
+    @Test
+    public void testDefaultNetworkEventSerialization() {
+        DefaultNetworkEvent ev = new DefaultNetworkEvent(1001);
+        ev.netId = 102;
+        ev.transports = 2;
+        ev.previousTransports = 4;
+        ev.ipv4 = true;
+        ev.initialScore = 20;
+        ev.finalScore = 60;
+        ev.durationMs = 54;
+        ev.validatedMs = 27;
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 102",
+                "  time_ms: 0",
+                "  transports: 2",
+                "  default_network_event <",
+                "    default_network_duration_ms: 54",
+                "    final_score: 60",
+                "    initial_score: 20",
+                "    ip_support: 1",
+                "    no_default_network_duration_ms: 0",
+                "    previous_default_network_link_layer: 1",
+                "    previous_network_ip_support: 0",
+                "    validation_duration_ms: 27",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, IpConnectivityEventBuilder.toProto(ev));
+    }
+
+    @Test
+    public void testDhcpClientEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(DhcpClientEvent.class),
+                aString("SomeState"),
+                anInt(192));
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 0",
+                "  network_id: 0",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  dhcp_event <",
+                "    duration_ms: 192",
+                "    if_name: \"\"",
+                "    state_transition: \"SomeState\"",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, ev);
+    }
+
+    @Test
+    public void testDhcpErrorEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(DhcpErrorEvent.class),
+                anInt(DhcpErrorEvent.L4_NOT_UDP));
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 0",
+                "  network_id: 0",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  dhcp_event <",
+                "    duration_ms: 0",
+                "    if_name: \"\"",
+                "    error_code: 50397184",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, ev);
+    }
+
+    @Test
+    public void testIpManagerEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(IpManagerEvent.class),
+                anInt(IpManagerEvent.PROVISIONING_OK),
+                aLong(5678));
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 0",
+                "  network_id: 0",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  ip_provisioning_event <",
+                "    event_type: 1",
+                "    if_name: \"\"",
+                "    latency_ms: 5678",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, ev);
+    }
+
+    @Test
+    public void testIpReachabilityEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(IpReachabilityEvent.class),
+                anInt(IpReachabilityEvent.NUD_FAILED));
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 0",
+                "  network_id: 0",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  ip_reachability_event <",
+                "    event_type: 512",
+                "    if_name: \"\"",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, ev);
+    }
+
+    @Test
+    public void testNetworkEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(NetworkEvent.class),
+                anInt(5),
+                aLong(20410));
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 0",
+                "  network_id: 0",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  network_event <",
+                "    event_type: 5",
+                "    latency_ms: 20410",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, ev);
+    }
+
+    @Test
+    public void testValidationProbeEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(ValidationProbeEvent.class),
+                aLong(40730),
+                anInt(ValidationProbeEvent.PROBE_HTTP),
+                anInt(204));
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 0",
+                "  network_id: 0",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  validation_probe_event <",
+                "    latency_ms: 40730",
+                "    probe_result: 204",
+                "    probe_type: 1",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, ev);
+    }
+
+    @Test
+    public void testApfProgramEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(ApfProgramEvent.class),
+                aLong(200),
+                aLong(18),
+                anInt(7),
+                anInt(9),
+                anInt(2048),
+                anInt(3));
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 0",
+                "  network_id: 0",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  apf_program_event <",
+                "    current_ras: 9",
+                "    drop_multicast: true",
+                "    effective_lifetime: 18",
+                "    filtered_ras: 7",
+                "    has_ipv4_addr: true",
+                "    lifetime: 200",
+                "    program_length: 2048",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, ev);
+    }
+
+    @Test
+    public void testApfStatsSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(ApfStats.class),
+                aLong(45000),
+                anInt(10),
+                anInt(2),
+                anInt(2),
+                anInt(1),
+                anInt(2),
+                anInt(4),
+                anInt(7),
+                anInt(3),
+                anInt(2048));
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 0",
+                "  network_id: 0",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  apf_statistics <",
+                "    dropped_ras: 2",
+                "    duration_ms: 45000",
+                "    matching_ras: 2",
+                "    max_program_size: 2048",
+                "    parse_errors: 2",
+                "    program_updates: 4",
+                "    program_updates_all: 7",
+                "    program_updates_allowing_multicast: 3",
+                "    received_ras: 10",
+                "    total_packet_dropped: 0",
+                "    total_packet_processed: 0",
+                "    zero_lifetime_ras: 1",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, ev);
+    }
+
+    @Test
+    public void testRaEventSerialization() {
+        ConnectivityMetricsEvent ev = describeIpEvent(
+                aType(RaEvent.class),
+                aLong(2000),
+                aLong(400),
+                aLong(300),
+                aLong(-1),
+                aLong(1000),
+                aLong(-1));
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 0",
+                "  network_id: 0",
+                "  time_ms: 1",
+                "  transports: 0",
+                "  ra_event <",
+                "    dnssl_lifetime: -1",
+                "    prefix_preferred_lifetime: 300",
+                "    prefix_valid_lifetime: 400",
+                "    rdnss_lifetime: 1000",
+                "    route_info_lifetime: -1",
+                "    router_lifetime: 2000",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, ev);
+    }
+
+    @Test
+    public void testWakeupStatsSerialization() {
+        WakeupStats stats = new WakeupStats("wlan0");
+        stats.totalWakeups = 14;
+        stats.applicationWakeups = 5;
+        stats.nonApplicationWakeups = 1;
+        stats.rootWakeups = 2;
+        stats.systemWakeups = 3;
+        stats.noUidWakeups = 3;
+        stats.l2UnicastCount = 5;
+        stats.l2MulticastCount = 1;
+        stats.l2BroadcastCount = 2;
+        stats.ethertypes.put(0x800, 3);
+        stats.ethertypes.put(0x86dd, 3);
+        stats.ipNextHeaders.put(6, 5);
+
+
+        IpConnectivityEvent got = IpConnectivityEventBuilder.toProto(stats);
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 0",
+                "  time_ms: 0",
+                "  transports: 0",
+                "  wakeup_stats <",
+                "    application_wakeups: 5",
+                "    duration_sec: 0",
+                "    ethertype_counts <",
+                "      key: 2048",
+                "      value: 3",
+                "    >",
+                "    ethertype_counts <",
+                "      key: 34525",
+                "      value: 3",
+                "    >",
+                "    ip_next_header_counts <",
+                "      key: 6",
+                "      value: 5",
+                "    >",
+                "    l2_broadcast_count: 2",
+                "    l2_multicast_count: 1",
+                "    l2_unicast_count: 5",
+                "    no_uid_wakeups: 3",
+                "    non_application_wakeups: 1",
+                "    root_wakeups: 2",
+                "    system_wakeups: 3",
+                "    total_wakeups: 14",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, got);
+    }
+
+    static void verifySerialization(String want, ConnectivityMetricsEvent... input) {
+        List<IpConnectivityEvent> protoInput =
+                IpConnectivityEventBuilder.toProto(Arrays.asList(input));
+        verifySerialization(want, protoInput.toArray(new IpConnectivityEvent[0]));
+    }
+
+    static void verifySerialization(String want, IpConnectivityEvent... input) {
+        try {
+            byte[] got = IpConnectivityEventBuilder.serialize(0, Arrays.asList(input));
+            IpConnectivityLog log = IpConnectivityLog.parseFrom(got);
+            assertEquals(want, log.toString());
+        } catch (Exception e) {
+            fail(e.toString());
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
new file mode 100644
index 0000000..8b072c4
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
@@ -0,0 +1,645 @@
+/*
+ * 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.connectivity;
+
+import static android.net.metrics.INetdEventListener.EVENT_GETADDRINFO;
+import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityMetricsEvent;
+import android.net.IIpConnectivityMetrics;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.RouteInfo;
+import android.net.metrics.ApfProgramEvent;
+import android.net.metrics.ApfStats;
+import android.net.metrics.DhcpClientEvent;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.IpManagerEvent;
+import android.net.metrics.IpReachabilityEvent;
+import android.net.metrics.RaEvent;
+import android.net.metrics.ValidationProbeEvent;
+import android.os.Parcelable;
+import android.system.OsConstants;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Base64;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.BitUtils;
+import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass;
+
+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.io.PrintWriter;
+import java.io.StringWriter;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpConnectivityMetricsTest {
+    static final IpReachabilityEvent FAKE_EV =
+            new IpReachabilityEvent(IpReachabilityEvent.NUD_FAILED);
+
+    private static final String EXAMPLE_IPV4 = "192.0.2.1";
+    private static final String EXAMPLE_IPV6 = "2001:db8:1200::2:1";
+
+    private static final byte[] MAC_ADDR =
+            {(byte)0x84, (byte)0xc9, (byte)0xb2, (byte)0x6a, (byte)0xed, (byte)0x4b};
+
+    @Mock Context mCtx;
+    @Mock IIpConnectivityMetrics mMockService;
+    @Mock ConnectivityManager mCm;
+
+    IpConnectivityMetrics mService;
+    NetdEventListenerService mNetdListener;
+    private static final NetworkCapabilities CAPABILITIES_WIFI = new NetworkCapabilities.Builder()
+            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+            .build();
+    private static final NetworkCapabilities CAPABILITIES_CELL = new NetworkCapabilities.Builder()
+            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+            .build();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mService = new IpConnectivityMetrics(mCtx, (ctx) -> 2000);
+        mNetdListener = new NetdEventListenerService(mCm);
+        mService.mNetdListener = mNetdListener;
+    }
+
+    @Test
+    public void testBufferFlushing() {
+        String output1 = getdump("flush");
+        assertEquals("", output1);
+
+        new IpConnectivityLog(mService.impl).log(1, FAKE_EV);
+        String output2 = getdump("flush");
+        assertFalse("".equals(output2));
+
+        String output3 = getdump("flush");
+        assertEquals("", output3);
+    }
+
+    @Test
+    public void testRateLimiting() {
+        final IpConnectivityLog logger = new IpConnectivityLog(mService.impl);
+        final ApfProgramEvent ev = new ApfProgramEvent.Builder().build();
+        final long fakeTimestamp = 1;
+
+        int attempt = 100; // More than burst quota, but less than buffer size.
+        for (int i = 0; i < attempt; i++) {
+            logger.log(ev);
+        }
+
+        String output1 = getdump("flush");
+        assertFalse("".equals(output1));
+
+        for (int i = 0; i < attempt; i++) {
+            assertFalse("expected event to be dropped", logger.log(fakeTimestamp, ev));
+        }
+
+        String output2 = getdump("flush");
+        assertEquals("", output2);
+    }
+
+    private void logDefaultNetworkEvent(long timeMs, NetworkAgentInfo nai,
+            NetworkAgentInfo oldNai) {
+        final Network network = (nai != null) ? nai.network() : null;
+        final int score = (nai != null) ? nai.getCurrentScore() : 0;
+        final boolean validated = (nai != null) ? nai.lastValidated : false;
+        final LinkProperties lp = (nai != null) ? nai.linkProperties : null;
+        final NetworkCapabilities nc = (nai != null) ? nai.networkCapabilities : null;
+
+        final Network prevNetwork = (oldNai != null) ? oldNai.network() : null;
+        final int prevScore = (oldNai != null) ? oldNai.getCurrentScore() : 0;
+        final LinkProperties prevLp = (oldNai != null) ? oldNai.linkProperties : null;
+        final NetworkCapabilities prevNc = (oldNai != null) ? oldNai.networkCapabilities : null;
+
+        mService.mDefaultNetworkMetrics.logDefaultNetworkEvent(timeMs, network, score, validated,
+                lp, nc, prevNetwork, prevScore, prevLp, prevNc);
+    }
+    @Test
+    public void testDefaultNetworkEvents() throws Exception {
+        final long cell = BitUtils.packBits(new int[]{NetworkCapabilities.TRANSPORT_CELLULAR});
+        final long wifi = BitUtils.packBits(new int[]{NetworkCapabilities.TRANSPORT_WIFI});
+
+        NetworkAgentInfo[][] defaultNetworks = {
+            // nothing -> cell
+            {null, makeNai(100, 10, false, true, cell)},
+            // cell -> wifi
+            {makeNai(100, 50, true, true, cell), makeNai(101, 20, true, false, wifi)},
+            // wifi -> nothing
+            {makeNai(101, 60, true, false, wifi), null},
+            // nothing -> cell
+            {null, makeNai(102, 10, true, true, cell)},
+            // cell -> wifi
+            {makeNai(102, 50, true, true, cell), makeNai(103, 20, true, false, wifi)},
+        };
+
+        long timeMs = mService.mDefaultNetworkMetrics.creationTimeMs;
+        long durationMs = 1001;
+        for (NetworkAgentInfo[] pair : defaultNetworks) {
+            timeMs += durationMs;
+            durationMs += durationMs;
+            logDefaultNetworkEvent(timeMs, pair[1], pair[0]);
+        }
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 5",
+                "  network_id: 0",
+                "  time_ms: 0",
+                "  transports: 0",
+                "  default_network_event <",
+                "    default_network_duration_ms: 1001",
+                "    final_score: 0",
+                "    initial_score: 0",
+                "    ip_support: 0",
+                "    no_default_network_duration_ms: 0",
+                "    previous_default_network_link_layer: 0",
+                "    previous_network_ip_support: 0",
+                "    validation_duration_ms: 0",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 2",
+                "  network_id: 100",
+                "  time_ms: 0",
+                "  transports: 1",
+                "  default_network_event <",
+                "    default_network_duration_ms: 2002",
+                "    final_score: 50",
+                "    initial_score: 10",
+                "    ip_support: 3",
+                "    no_default_network_duration_ms: 0",
+                "    previous_default_network_link_layer: 0",
+                "    previous_network_ip_support: 0",
+                "    validation_duration_ms: 2002",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 101",
+                "  time_ms: 0",
+                "  transports: 2",
+                "  default_network_event <",
+                "    default_network_duration_ms: 4004",
+                "    final_score: 60",
+                "    initial_score: 20",
+                "    ip_support: 1",
+                "    no_default_network_duration_ms: 0",
+                "    previous_default_network_link_layer: 2",
+                "    previous_network_ip_support: 0",
+                "    validation_duration_ms: 4004",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 5",
+                "  network_id: 0",
+                "  time_ms: 0",
+                "  transports: 0",
+                "  default_network_event <",
+                "    default_network_duration_ms: 8008",
+                "    final_score: 0",
+                "    initial_score: 0",
+                "    ip_support: 0",
+                "    no_default_network_duration_ms: 0",
+                "    previous_default_network_link_layer: 4",
+                "    previous_network_ip_support: 0",
+                "    validation_duration_ms: 0",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 2",
+                "  network_id: 102",
+                "  time_ms: 0",
+                "  transports: 1",
+                "  default_network_event <",
+                "    default_network_duration_ms: 16016",
+                "    final_score: 50",
+                "    initial_score: 10",
+                "    ip_support: 3",
+                "    no_default_network_duration_ms: 0",
+                "    previous_default_network_link_layer: 4",
+                "    previous_network_ip_support: 0",
+                "    validation_duration_ms: 16016",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, getdump("flush"));
+    }
+
+    @Test
+    public void testEndToEndLogging() throws Exception {
+        // TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto.
+        IpConnectivityLog logger = new IpConnectivityLog(mService.impl);
+
+        ApfStats apfStats = new ApfStats.Builder()
+                .setDurationMs(45000)
+                .setReceivedRas(10)
+                .setMatchingRas(2)
+                .setDroppedRas(2)
+                .setParseErrors(2)
+                .setZeroLifetimeRas(1)
+                .setProgramUpdates(4)
+                .setProgramUpdatesAll(7)
+                .setProgramUpdatesAllowingMulticast(3)
+                .setMaxProgramSize(2048)
+                .build();
+
+        final ValidationProbeEvent validationEv = new ValidationProbeEvent.Builder()
+                .setDurationMs(40730)
+                .setProbeType(ValidationProbeEvent.PROBE_HTTP, true)
+                .setReturnCode(204)
+                .build();
+
+        final DhcpClientEvent event = new DhcpClientEvent.Builder()
+                .setMsg("SomeState")
+                .setDurationMs(192)
+                .build();
+        Parcelable[] events = {
+            new IpReachabilityEvent(IpReachabilityEvent.NUD_FAILED), event,
+            new IpManagerEvent(IpManagerEvent.PROVISIONING_OK, 5678),
+            validationEv,
+            apfStats,
+            new RaEvent(2000, 400, 300, -1, 1000, -1)
+        };
+
+        for (int i = 0; i < events.length; i++) {
+            ConnectivityMetricsEvent ev = new ConnectivityMetricsEvent();
+            ev.timestamp = 100 * (i + 1);
+            ev.ifname = "wlan0";
+            ev.data = events[i];
+            logger.log(ev);
+        }
+
+        // netId, errno, latency, destination
+        connectEvent(100, OsConstants.EALREADY, 0, EXAMPLE_IPV4);
+        connectEvent(100, OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6);
+        connectEvent(100, 0, 110, EXAMPLE_IPV4);
+        connectEvent(101, 0, 23, EXAMPLE_IPV4);
+        connectEvent(101, 0, 45, EXAMPLE_IPV6);
+        connectEvent(100, OsConstants.EAGAIN, 0, EXAMPLE_IPV4);
+
+        // netId, type, return code, latency
+        dnsEvent(100, EVENT_GETADDRINFO, 0, 3456);
+        dnsEvent(100, EVENT_GETADDRINFO, 3, 45);
+        dnsEvent(100, EVENT_GETHOSTBYNAME, 0, 638);
+        dnsEvent(101, EVENT_GETADDRINFO, 0, 56);
+        dnsEvent(101, EVENT_GETHOSTBYNAME, 0, 34);
+
+        // iface, uid
+        final byte[] mac = {0x48, 0x7c, 0x2b, 0x6a, 0x3e, 0x4b};
+        final String srcIp = "192.168.2.1";
+        final String dstIp = "192.168.2.23";
+        final int sport = 2356;
+        final int dport = 13489;
+        final long now = 1001L;
+        final int v4 = 0x800;
+        final int tcp = 6;
+        final int udp = 17;
+        wakeupEvent("wlan0", 1000, v4, tcp, mac, srcIp, dstIp, sport, dport, 1001L);
+        wakeupEvent("wlan0", 10123, v4, tcp, mac, srcIp, dstIp, sport, dport, 1001L);
+        wakeupEvent("wlan0", 1000, v4, udp, mac, srcIp, dstIp, sport, dport, 1001L);
+        wakeupEvent("wlan0", 10008, v4, udp, mac, srcIp, dstIp, sport, dport, 1001L);
+        wakeupEvent("wlan0", -1, v4, udp, mac, srcIp, dstIp, sport, dport, 1001L);
+        wakeupEvent("wlan0", 10008, v4, tcp, mac, srcIp, dstIp, sport, dport, 1001L);
+
+        long timeMs = mService.mDefaultNetworkMetrics.creationTimeMs;
+        final long cell = BitUtils.packBits(new int[]{NetworkCapabilities.TRANSPORT_CELLULAR});
+        final long wifi = BitUtils.packBits(new int[]{NetworkCapabilities.TRANSPORT_WIFI});
+        NetworkAgentInfo cellNai = makeNai(100, 50, false, true, cell);
+        NetworkAgentInfo wifiNai = makeNai(101, 60, true, false, wifi);
+        logDefaultNetworkEvent(timeMs + 200L, cellNai, null);
+        logDefaultNetworkEvent(timeMs + 300L, wifiNai, cellNai);
+
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 0",
+                "  time_ms: 100",
+                "  transports: 0",
+                "  ip_reachability_event <",
+                "    event_type: 512",
+                "    if_name: \"\"",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 0",
+                "  time_ms: 200",
+                "  transports: 0",
+                "  dhcp_event <",
+                "    duration_ms: 192",
+                "    if_name: \"\"",
+                "    state_transition: \"SomeState\"",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 0",
+                "  time_ms: 300",
+                "  transports: 0",
+                "  ip_provisioning_event <",
+                "    event_type: 1",
+                "    if_name: \"\"",
+                "    latency_ms: 5678",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 0",
+                "  time_ms: 400",
+                "  transports: 0",
+                "  validation_probe_event <",
+                "    latency_ms: 40730",
+                "    probe_result: 204",
+                "    probe_type: 257",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 0",
+                "  time_ms: 500",
+                "  transports: 0",
+                "  apf_statistics <",
+                "    dropped_ras: 2",
+                "    duration_ms: 45000",
+                "    matching_ras: 2",
+                "    max_program_size: 2048",
+                "    parse_errors: 2",
+                "    program_updates: 4",
+                "    program_updates_all: 7",
+                "    program_updates_allowing_multicast: 3",
+                "    received_ras: 10",
+                "    total_packet_dropped: 0",
+                "    total_packet_processed: 0",
+                "    zero_lifetime_ras: 1",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 0",
+                "  time_ms: 600",
+                "  transports: 0",
+                "  ra_event <",
+                "    dnssl_lifetime: -1",
+                "    prefix_preferred_lifetime: 300",
+                "    prefix_valid_lifetime: 400",
+                "    rdnss_lifetime: 1000",
+                "    route_info_lifetime: -1",
+                "    router_lifetime: 2000",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 5",
+                "  network_id: 0",
+                "  time_ms: 0",
+                "  transports: 0",
+                "  default_network_event <",
+                "    default_network_duration_ms: 200",
+                "    final_score: 0",
+                "    initial_score: 0",
+                "    ip_support: 0",
+                "    no_default_network_duration_ms: 0",
+                "    previous_default_network_link_layer: 0",
+                "    previous_network_ip_support: 0",
+                "    validation_duration_ms: 0",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 2",
+                "  network_id: 100",
+                "  time_ms: 0",
+                "  transports: 1",
+                "  default_network_event <",
+                "    default_network_duration_ms: 100",
+                "    final_score: 50",
+                "    initial_score: 50",
+                "    ip_support: 2",
+                "    no_default_network_duration_ms: 0",
+                "    previous_default_network_link_layer: 0",
+                "    previous_network_ip_support: 0",
+                "    validation_duration_ms: 100",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 100",
+                "  time_ms: 0",
+                "  transports: 2",
+                "  connect_statistics <",
+                "    connect_blocking_count: 1",
+                "    connect_count: 3",
+                "    errnos_counters <",
+                "      key: 11",
+                "      value: 1",
+                "    >",
+                "    ipv6_addr_count: 1",
+                "    latencies_ms: 110",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 2",
+                "  network_id: 101",
+                "  time_ms: 0",
+                "  transports: 1",
+                "  connect_statistics <",
+                "    connect_blocking_count: 2",
+                "    connect_count: 2",
+                "    ipv6_addr_count: 1",
+                "    latencies_ms: 23",
+                "    latencies_ms: 45",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 100",
+                "  time_ms: 0",
+                "  transports: 2",
+                "  dns_lookup_batch <",
+                "    event_types: 1",
+                "    event_types: 1",
+                "    event_types: 2",
+                "    getaddrinfo_error_count: 0",
+                "    getaddrinfo_query_count: 0",
+                "    gethostbyname_error_count: 0",
+                "    gethostbyname_query_count: 0",
+                "    latencies_ms: 3456",
+                "    latencies_ms: 45",
+                "    latencies_ms: 638",
+                "    return_codes: 0",
+                "    return_codes: 3",
+                "    return_codes: 0",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 2",
+                "  network_id: 101",
+                "  time_ms: 0",
+                "  transports: 1",
+                "  dns_lookup_batch <",
+                "    event_types: 1",
+                "    event_types: 2",
+                "    getaddrinfo_error_count: 0",
+                "    getaddrinfo_query_count: 0",
+                "    gethostbyname_error_count: 0",
+                "    gethostbyname_query_count: 0",
+                "    latencies_ms: 56",
+                "    latencies_ms: 34",
+                "    return_codes: 0",
+                "    return_codes: 0",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 0",
+                "  time_ms: 0",
+                "  transports: 0",
+                "  wakeup_stats <",
+                "    application_wakeups: 3",
+                "    duration_sec: 0",
+                "    ethertype_counts <",
+                "      key: 2048",
+                "      value: 6",
+                "    >",
+                "    ip_next_header_counts <",
+                "      key: 6",
+                "      value: 3",
+                "    >",
+                "    ip_next_header_counts <",
+                "      key: 17",
+                "      value: 3",
+                "    >",
+                "    l2_broadcast_count: 0",
+                "    l2_multicast_count: 0",
+                "    l2_unicast_count: 6",
+                "    no_uid_wakeups: 1",
+                "    non_application_wakeups: 0",
+                "    root_wakeups: 0",
+                "    system_wakeups: 2",
+                "    total_wakeups: 6",
+                "  >",
+                ">",
+                "version: 2\n");
+
+        verifySerialization(want, getdump("flush"));
+    }
+
+    String getdump(String ... command) {
+        StringWriter buffer = new StringWriter();
+        PrintWriter writer = new PrintWriter(buffer);
+        mService.impl.dump(null, writer, command);
+        return buffer.toString();
+    }
+
+    private void setCapabilities(int netId) {
+        final ArgumentCaptor<ConnectivityManager.NetworkCallback> networkCallback =
+                ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
+        verify(mCm).registerNetworkCallback(any(), networkCallback.capture());
+        networkCallback.getValue().onCapabilitiesChanged(new Network(netId),
+                netId == 100 ? CAPABILITIES_WIFI : CAPABILITIES_CELL);
+    }
+
+    void connectEvent(int netId, int error, int latencyMs, String ipAddr) throws Exception {
+        setCapabilities(netId);
+        mNetdListener.onConnectEvent(netId, error, latencyMs, ipAddr, 80, 1);
+    }
+
+    void dnsEvent(int netId, int type, int result, int latency) throws Exception {
+        setCapabilities(netId);
+        mNetdListener.onDnsEvent(netId, type, result, latency, "", null, 0, 0);
+    }
+
+    void wakeupEvent(String iface, int uid, int ether, int ip, byte[] mac, String srcIp,
+            String dstIp, int sport, int dport, long now) throws Exception {
+        String prefix = NetdEventListenerService.WAKEUP_EVENT_IFACE_PREFIX + iface;
+        mNetdListener.onWakeupEvent(prefix, uid, ether, ip, mac, srcIp, dstIp, sport, dport, now);
+    }
+
+    NetworkAgentInfo makeNai(int netId, int score, boolean ipv4, boolean ipv6, long transports) {
+        NetworkAgentInfo nai = mock(NetworkAgentInfo.class);
+        when(nai.network()).thenReturn(new Network(netId));
+        when(nai.getCurrentScore()).thenReturn(score);
+        nai.linkProperties = new LinkProperties();
+        nai.networkCapabilities = new NetworkCapabilities();
+        nai.lastValidated = true;
+        for (int t : BitUtils.unpackBits(transports)) {
+            nai.networkCapabilities.addTransportType(t);
+        }
+        if (ipv4) {
+            nai.linkProperties.addLinkAddress(new LinkAddress("192.0.2.12/24"));
+            nai.linkProperties.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0")));
+        }
+        if (ipv6) {
+            nai.linkProperties.addLinkAddress(new LinkAddress("2001:db8:dead:beef:f00::a0/64"));
+            nai.linkProperties.addRoute(new RouteInfo(new IpPrefix("::/0")));
+        }
+        return nai;
+    }
+
+
+
+    static void verifySerialization(String want, String output) {
+        try {
+            byte[] got = Base64.decode(output, Base64.DEFAULT);
+            IpConnectivityLogClass.IpConnectivityLog log =
+                    IpConnectivityLogClass.IpConnectivityLog.parseFrom(got);
+            assertEquals(want, log.toString());
+        } catch (Exception e) {
+            fail(e.toString());
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
new file mode 100644
index 0000000..36e229d
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -0,0 +1,395 @@
+/*
+ * 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.connectivity;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityResources;
+import android.net.IDnsResolver;
+import android.net.INetd;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.os.Binder;
+import android.text.format.DateUtils;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.server.ConnectivityService;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class LingerMonitorTest {
+    static final String CELLULAR = "CELLULAR";
+    static final String WIFI     = "WIFI";
+
+    static final long LOW_RATE_LIMIT = DateUtils.MINUTE_IN_MILLIS;
+    static final long HIGH_RATE_LIMIT = 0;
+
+    static final int LOW_DAILY_LIMIT = 2;
+    static final int HIGH_DAILY_LIMIT = 1000;
+
+    private static final int TEST_LINGER_DELAY_MS = 400;
+
+    LingerMonitor mMonitor;
+
+    @Mock ConnectivityService mConnService;
+    @Mock IDnsResolver mDnsResolver;
+    @Mock INetd mNetd;
+    @Mock Context mCtx;
+    @Mock NetworkNotificationManager mNotifier;
+    @Mock Resources mResources;
+    @Mock QosCallbackTracker mQosCallbackTracker;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mCtx.getResources()).thenReturn(mResources);
+        when(mCtx.getPackageName()).thenReturn("com.android.server.connectivity");
+        ConnectivityResources.setResourcesContextForTest(mCtx);
+
+        mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, HIGH_RATE_LIMIT);
+    }
+
+    @After
+    public void tearDown() {
+        ConnectivityResources.setResourcesContextForTest(null);
+    }
+
+    @Test
+    public void testTransitions() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        NetworkAgentInfo nai1 = wifiNai(100);
+        NetworkAgentInfo nai2 = cellNai(101);
+
+        assertTrue(mMonitor.isNotificationEnabled(nai1, nai2));
+        assertFalse(mMonitor.isNotificationEnabled(nai2, nai1));
+    }
+
+    @Test
+    public void testNotificationOnLinger() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNotification(from, to);
+    }
+
+    @Test
+    public void testToastOnLinger() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyToast(from, to);
+    }
+
+    @Test
+    public void testNotificationClearedAfterDisconnect() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNotification(from, to);
+
+        mMonitor.noteDisconnect(to);
+        verify(mNotifier, times(1)).clearNotification(100);
+    }
+
+    @Test
+    public void testNotificationClearedAfterSwitchingBack() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNotification(from, to);
+
+        mMonitor.noteLingerDefaultNetwork(to, from);
+        verify(mNotifier, times(1)).clearNotification(100);
+    }
+
+    @Test
+    public void testUniqueToast() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyToast(from, to);
+
+        mMonitor.noteLingerDefaultNetwork(to, from);
+        verify(mNotifier, times(1)).clearNotification(100);
+
+        reset(mNotifier);
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNoNotifications();
+    }
+
+    @Test
+    public void testMultipleNotifications() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo wifi1 = wifiNai(100);
+        NetworkAgentInfo wifi2 = wifiNai(101);
+        NetworkAgentInfo cell = cellNai(102);
+
+        mMonitor.noteLingerDefaultNetwork(wifi1, cell);
+        verifyNotification(wifi1, cell);
+
+        mMonitor.noteLingerDefaultNetwork(cell, wifi2);
+        verify(mNotifier, times(1)).clearNotification(100);
+
+        reset(mNotifier);
+        mMonitor.noteLingerDefaultNetwork(wifi2, cell);
+        verifyNotification(wifi2, cell);
+    }
+
+    @Test
+    public void testRateLimiting() throws InterruptedException {
+        mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, LOW_RATE_LIMIT);
+
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo wifi1 = wifiNai(100);
+        NetworkAgentInfo wifi2 = wifiNai(101);
+        NetworkAgentInfo wifi3 = wifiNai(102);
+        NetworkAgentInfo cell = cellNai(103);
+
+        mMonitor.noteLingerDefaultNetwork(wifi1, cell);
+        verifyNotification(wifi1, cell);
+        reset(mNotifier);
+
+        Thread.sleep(50);
+        mMonitor.noteLingerDefaultNetwork(cell, wifi2);
+        mMonitor.noteLingerDefaultNetwork(wifi2, cell);
+        verifyNoNotifications();
+
+        Thread.sleep(50);
+        mMonitor.noteLingerDefaultNetwork(cell, wifi3);
+        mMonitor.noteLingerDefaultNetwork(wifi3, cell);
+        verifyNoNotifications();
+    }
+
+    @Test
+    public void testDailyLimiting() throws InterruptedException {
+        mMonitor = new TestableLingerMonitor(mCtx, mNotifier, LOW_DAILY_LIMIT, HIGH_RATE_LIMIT);
+
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo wifi1 = wifiNai(100);
+        NetworkAgentInfo wifi2 = wifiNai(101);
+        NetworkAgentInfo wifi3 = wifiNai(102);
+        NetworkAgentInfo cell = cellNai(103);
+
+        mMonitor.noteLingerDefaultNetwork(wifi1, cell);
+        verifyNotification(wifi1, cell);
+        reset(mNotifier);
+
+        Thread.sleep(50);
+        mMonitor.noteLingerDefaultNetwork(cell, wifi2);
+        mMonitor.noteLingerDefaultNetwork(wifi2, cell);
+        verifyNotification(wifi2, cell);
+        reset(mNotifier);
+
+        Thread.sleep(50);
+        mMonitor.noteLingerDefaultNetwork(cell, wifi3);
+        mMonitor.noteLingerDefaultNetwork(wifi3, cell);
+        verifyNoNotifications();
+    }
+
+    @Test
+    public void testUniqueNotification() {
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNotification(from, to);
+
+        mMonitor.noteLingerDefaultNetwork(to, from);
+        verify(mNotifier, times(1)).clearNotification(100);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNotification(from, to);
+    }
+
+    @Test
+    public void testIgnoreNeverValidatedNetworks() {
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+        from.everValidated = false;
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNoNotifications();
+    }
+
+    @Test
+    public void testIgnoreCurrentlyValidatedNetworks() {
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+        from.lastValidated = true;
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNoNotifications();
+    }
+
+    @Test
+    public void testNoNotificationType() {
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+        setNotificationSwitch();
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNoNotifications();
+    }
+
+    @Test
+    public void testNoTransitionToNotify() {
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_NONE);
+        setNotificationSwitch(transition(WIFI, CELLULAR));
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNoNotifications();
+    }
+
+    @Test
+    public void testDifferentTransitionToNotify() {
+        setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+        setNotificationSwitch(transition(CELLULAR, WIFI));
+        NetworkAgentInfo from = wifiNai(100);
+        NetworkAgentInfo to = cellNai(101);
+
+        mMonitor.noteLingerDefaultNetwork(from, to);
+        verifyNoNotifications();
+    }
+
+    void setNotificationSwitch(String... transitions) {
+        when(mResources.getStringArray(R.array.config_networkNotifySwitches))
+                .thenReturn(transitions);
+    }
+
+    String transition(String from, String to) {
+        return from + "-" + to;
+    }
+
+    void setNotificationType(int type) {
+        when(mResources.getInteger(R.integer.config_networkNotifySwitchType)).thenReturn(type);
+    }
+
+    void verifyNoToast() {
+        verify(mNotifier, never()).showToast(any(), any());
+    }
+
+    void verifyNoNotification() {
+        verify(mNotifier, never())
+                .showNotification(anyInt(), any(), any(), any(), any(), anyBoolean());
+    }
+
+    void verifyNoNotifications() {
+        verifyNoToast();
+        verifyNoNotification();
+    }
+
+    void verifyToast(NetworkAgentInfo from, NetworkAgentInfo to) {
+        verifyNoNotification();
+        verify(mNotifier, times(1)).showToast(from, to);
+    }
+
+    void verifyNotification(NetworkAgentInfo from, NetworkAgentInfo to) {
+        verifyNoToast();
+        verify(mNotifier, times(1)).showNotification(eq(from.network.netId),
+                eq(NotificationType.NETWORK_SWITCH), eq(from), eq(to), any(), eq(true));
+    }
+
+    NetworkAgentInfo nai(int netId, int transport, int networkType, String networkTypeName) {
+        NetworkInfo info = new NetworkInfo(networkType, 0, networkTypeName, "");
+        NetworkCapabilities caps = new NetworkCapabilities();
+        caps.addCapability(0);
+        caps.addTransportType(transport);
+        NetworkAgentInfo nai = new NetworkAgentInfo(null, new Network(netId), info,
+                new LinkProperties(), caps, new NetworkScore.Builder().setLegacyInt(50).build(),
+                mCtx, null, new NetworkAgentConfig.Builder().build(), mConnService, mNetd,
+                mDnsResolver, NetworkProvider.ID_NONE, Binder.getCallingUid(), TEST_LINGER_DELAY_MS,
+                mQosCallbackTracker, new ConnectivityService.Dependencies());
+        nai.everValidated = true;
+        return nai;
+    }
+
+    NetworkAgentInfo wifiNai(int netId) {
+        return nai(netId, NetworkCapabilities.TRANSPORT_WIFI,
+                ConnectivityManager.TYPE_WIFI, WIFI);
+    }
+
+    NetworkAgentInfo cellNai(int netId) {
+        return nai(netId, NetworkCapabilities.TRANSPORT_CELLULAR,
+                ConnectivityManager.TYPE_MOBILE, CELLULAR);
+    }
+
+    public static class TestableLingerMonitor extends LingerMonitor {
+        public TestableLingerMonitor(Context c, NetworkNotificationManager n, int l, long r) {
+            super(c, n, l, r);
+        }
+        @Override protected PendingIntent createNotificationIntent() {
+            return null;
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/MetricsTestUtil.java b/tests/unit/java/com/android/server/connectivity/MetricsTestUtil.java
new file mode 100644
index 0000000..5064b9b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/MetricsTestUtil.java
@@ -0,0 +1,81 @@
+/*
+ * 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.connectivity;
+
+import android.net.ConnectivityMetricsEvent;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.function.Consumer;
+
+abstract public class MetricsTestUtil {
+    private MetricsTestUtil() {
+    }
+
+    static ConnectivityMetricsEvent ev(Parcelable p) {
+        ConnectivityMetricsEvent ev = new ConnectivityMetricsEvent();
+        ev.timestamp = 1L;
+        ev.data = p;
+        return ev;
+    }
+
+    static ConnectivityMetricsEvent describeIpEvent(Consumer<Parcel>... fs) {
+        Parcel p = Parcel.obtain();
+        for (Consumer<Parcel> f : fs) {
+            f.accept(p);
+        }
+        p.setDataPosition(0);
+        return ev(p.readParcelable(ClassLoader.getSystemClassLoader()));
+    }
+
+    static Consumer<Parcel> aType(Class<?> c) {
+        return aString(c.getName());
+    }
+
+    static Consumer<Parcel> aBool(boolean b) {
+        return aByte((byte) (b ? 1 : 0));
+    }
+
+    static Consumer<Parcel> aByte(byte b) {
+        return (p) -> p.writeByte(b);
+    }
+
+    static Consumer<Parcel> anInt(int i) {
+        return (p) -> p.writeInt(i);
+    }
+
+    static Consumer<Parcel> aLong(long l) {
+        return (p) -> p.writeLong(l);
+    }
+
+    static Consumer<Parcel> aString(String s) {
+        return (p) -> p.writeString(s);
+    }
+
+    static Consumer<Parcel> aByteArray(byte... ary) {
+        return (p) -> p.writeByteArray(ary);
+    }
+
+    static Consumer<Parcel> anIntArray(int... ary) {
+        return (p) -> p.writeIntArray(ary);
+    }
+
+    static byte b(int i) {
+        return (byte) i;
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
new file mode 100644
index 0000000..4c80f6a
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
@@ -0,0 +1,386 @@
+/*
+ * 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.connectivity;
+
+import static android.content.Intent.ACTION_CONFIGURATION_CHANGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkPolicy.LIMIT_DISABLED;
+import static android.net.NetworkPolicy.SNOOZE_NEVER;
+import static android.net.NetworkPolicy.WARNING_DISABLED;
+import static android.provider.Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES;
+
+import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
+import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN;
+
+import static junit.framework.TestCase.assertNotNull;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.usage.NetworkStatsManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.EthernetNetworkSpecifier;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkPolicy;
+import android.net.NetworkPolicyManager;
+import android.net.NetworkTemplate;
+import android.net.TelephonyNetworkSpecifier;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.test.mock.MockContentResolver;
+import android.util.DataUnit;
+import android.util.RecurrenceRule;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.server.LocalServices;
+import com.android.server.net.NetworkPolicyManagerInternal;
+import com.android.server.net.NetworkStatsManagerInternal;
+
+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.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.time.Period;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MultipathPolicyTrackerTest {
+    private static final Network TEST_NETWORK = new Network(123);
+    private static final int POLICY_SNOOZED = -100;
+
+    @Mock private Context mContext;
+    @Mock private Context mUserAllContext;
+    @Mock private Resources mResources;
+    @Mock private Handler mHandler;
+    @Mock private MultipathPolicyTracker.Dependencies mDeps;
+    @Mock private Clock mClock;
+    @Mock private ConnectivityManager mCM;
+    @Mock private NetworkPolicyManager mNPM;
+    @Mock private NetworkStatsManager mStatsManager;
+    @Mock private NetworkPolicyManagerInternal mNPMI;
+    @Mock private NetworkStatsManagerInternal mNetworkStatsManagerInternal;
+    @Mock private TelephonyManager mTelephonyManager;
+    private MockContentResolver mContentResolver;
+
+    private ArgumentCaptor<BroadcastReceiver> mConfigChangeReceiverCaptor;
+
+    private MultipathPolicyTracker mTracker;
+
+    private Clock mPreviousRecurrenceRuleClock;
+    private boolean mRecurrenceRuleClockMocked;
+
+    private <T> void mockService(String serviceName, Class<T> serviceClass, T service) {
+        doReturn(serviceName).when(mContext).getSystemServiceName(serviceClass);
+        doReturn(service).when(mContext).getSystemService(serviceName);
+        if (mContext.getSystemService(serviceClass) == null) {
+            // Test is using mockito-extended
+            doCallRealMethod().when(mContext).getSystemService(serviceClass);
+        }
+    }
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mPreviousRecurrenceRuleClock = RecurrenceRule.sClock;
+        RecurrenceRule.sClock = mClock;
+        mRecurrenceRuleClockMocked = true;
+
+        mConfigChangeReceiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class);
+
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo());
+        // Mock user id to all users that Context#registerReceiver will register with all users too.
+        doReturn(UserHandle.ALL.getIdentifier()).when(mUserAllContext).getUserId();
+        when(mContext.createContextAsUser(eq(UserHandle.ALL), anyInt()))
+                .thenReturn(mUserAllContext);
+        when(mUserAllContext.registerReceiver(mConfigChangeReceiverCaptor.capture(),
+                argThat(f -> f.hasAction(ACTION_CONFIGURATION_CHANGED)), any(), any()))
+                .thenReturn(null);
+
+        when(mDeps.getClock()).thenReturn(mClock);
+
+        when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager);
+
+        mContentResolver = Mockito.spy(new MockContentResolver(mContext));
+        mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+        Settings.Global.clearProviderForTest();
+        when(mContext.getContentResolver()).thenReturn(mContentResolver);
+
+        mockService(Context.CONNECTIVITY_SERVICE, ConnectivityManager.class, mCM);
+        mockService(Context.NETWORK_POLICY_SERVICE, NetworkPolicyManager.class, mNPM);
+        mockService(Context.NETWORK_STATS_SERVICE, NetworkStatsManager.class, mStatsManager);
+        mockService(Context.TELEPHONY_SERVICE, TelephonyManager.class, mTelephonyManager);
+
+        LocalServices.removeServiceForTest(NetworkPolicyManagerInternal.class);
+        LocalServices.addService(NetworkPolicyManagerInternal.class, mNPMI);
+
+        LocalServices.removeServiceForTest(NetworkStatsManagerInternal.class);
+        LocalServices.addService(NetworkStatsManagerInternal.class, mNetworkStatsManagerInternal);
+
+        mTracker = new MultipathPolicyTracker(mContext, mHandler, mDeps);
+    }
+
+    @After
+    public void tearDown() {
+        // Avoid setting static clock to null (which should normally not be the case)
+        // if MockitoAnnotations.initMocks threw an exception
+        if (mRecurrenceRuleClockMocked) {
+            RecurrenceRule.sClock = mPreviousRecurrenceRuleClock;
+        }
+        mRecurrenceRuleClockMocked = false;
+    }
+
+    private void setDefaultQuotaGlobalSetting(long setting) {
+        Settings.Global.putInt(mContentResolver, NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES,
+                (int) setting);
+    }
+
+    private void testGetMultipathPreference(
+            long usedBytesToday, long subscriptionQuota, long policyWarning, long policyLimit,
+            long defaultGlobalSetting, long defaultResSetting, boolean roaming) {
+
+        // TODO: tests should not use ZoneId.systemDefault() once code handles TZ correctly.
+        final ZonedDateTime now = ZonedDateTime.ofInstant(
+                Instant.parse("2017-04-02T10:11:12Z"), ZoneId.systemDefault());
+        final ZonedDateTime startOfDay = now.truncatedTo(ChronoUnit.DAYS);
+        when(mClock.millis()).thenReturn(now.toInstant().toEpochMilli());
+        when(mClock.instant()).thenReturn(now.toInstant());
+        when(mClock.getZone()).thenReturn(ZoneId.systemDefault());
+
+        // Setup plan quota
+        when(mNPMI.getSubscriptionOpportunisticQuota(TEST_NETWORK, QUOTA_TYPE_MULTIPATH))
+                .thenReturn(subscriptionQuota);
+
+        // Setup user policy warning / limit
+        if (policyWarning != WARNING_DISABLED || policyLimit != LIMIT_DISABLED) {
+            final Instant recurrenceStart = Instant.parse("2017-04-01T00:00:00Z");
+            final RecurrenceRule recurrenceRule = new RecurrenceRule(
+                    ZonedDateTime.ofInstant(
+                            recurrenceStart,
+                            ZoneId.systemDefault()),
+                    null /* end */,
+                    Period.ofMonths(1));
+            final boolean snoozeWarning = policyWarning == POLICY_SNOOZED;
+            final boolean snoozeLimit = policyLimit == POLICY_SNOOZED;
+            when(mNPM.getNetworkPolicies()).thenReturn(new NetworkPolicy[] {
+                    new NetworkPolicy(
+                            NetworkTemplate.buildTemplateMobileWildcard(),
+                            recurrenceRule,
+                            snoozeWarning ? 0 : policyWarning,
+                            snoozeLimit ? 0 : policyLimit,
+                            snoozeWarning ? recurrenceStart.toEpochMilli() + 1 : SNOOZE_NEVER,
+                            snoozeLimit ? recurrenceStart.toEpochMilli() + 1 : SNOOZE_NEVER,
+                            SNOOZE_NEVER,
+                            true /* metered */,
+                            false /* inferred */)
+            });
+        } else {
+            when(mNPM.getNetworkPolicies()).thenReturn(new NetworkPolicy[0]);
+        }
+
+        // Setup default quota in settings and resources
+        if (defaultGlobalSetting > 0) {
+            setDefaultQuotaGlobalSetting(defaultGlobalSetting);
+        }
+        when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes))
+                .thenReturn((int) defaultResSetting);
+
+        when(mNetworkStatsManagerInternal.getNetworkTotalBytes(
+                any(),
+                eq(startOfDay.toInstant().toEpochMilli()),
+                eq(now.toInstant().toEpochMilli()))).thenReturn(usedBytesToday);
+
+        ArgumentCaptor<ConnectivityManager.NetworkCallback> networkCallback =
+                ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
+        mTracker.start();
+        verify(mCM).registerNetworkCallback(any(), networkCallback.capture(), any());
+
+        // Simulate callback after capability changes
+        NetworkCapabilities capabilities = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addTransportType(TRANSPORT_CELLULAR)
+                .setNetworkSpecifier(new EthernetNetworkSpecifier("eth234"));
+        if (!roaming) {
+            capabilities.addCapability(NET_CAPABILITY_NOT_ROAMING);
+        }
+        networkCallback.getValue().onCapabilitiesChanged(
+                TEST_NETWORK,
+                capabilities);
+
+        // make sure it also works with the new introduced  TelephonyNetworkSpecifier
+        capabilities = new NetworkCapabilities()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addTransportType(TRANSPORT_CELLULAR)
+                .setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
+                        .setSubscriptionId(234).build());
+        if (!roaming) {
+            capabilities.addCapability(NET_CAPABILITY_NOT_ROAMING);
+        }
+        networkCallback.getValue().onCapabilitiesChanged(
+                TEST_NETWORK,
+                capabilities);
+    }
+
+    @Test
+    public void testGetMultipathPreference_SubscriptionQuota() {
+        testGetMultipathPreference(
+                DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
+                DataUnit.MEGABYTES.toBytes(14) /* subscriptionQuota */,
+                DataUnit.MEGABYTES.toBytes(100) /* policyWarning */,
+                LIMIT_DISABLED,
+                DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
+                2_500_000 /* defaultResSetting */,
+                false /* roaming */);
+
+        verify(mStatsManager, times(1)).registerUsageCallback(
+                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
+    }
+
+    @Test
+    public void testGetMultipathPreference_UserWarningQuota() {
+        testGetMultipathPreference(
+                DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
+                OPPORTUNISTIC_QUOTA_UNKNOWN,
+                // 29 days from Apr. 2nd to May 1st
+                DataUnit.MEGABYTES.toBytes(15 * 29 * 20) /* policyWarning */,
+                LIMIT_DISABLED,
+                DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
+                2_500_000 /* defaultResSetting */,
+                false /* roaming */);
+
+        // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB
+        verify(mStatsManager, times(1)).registerUsageCallback(
+                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
+    }
+
+    @Test
+    public void testGetMultipathPreference_SnoozedWarningQuota() {
+        testGetMultipathPreference(
+                DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
+                OPPORTUNISTIC_QUOTA_UNKNOWN,
+                // 29 days from Apr. 2nd to May 1st
+                POLICY_SNOOZED /* policyWarning */,
+                DataUnit.MEGABYTES.toBytes(15 * 29 * 20) /* policyLimit */,
+                DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
+                2_500_000 /* defaultResSetting */,
+                false /* roaming */);
+
+        // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB
+        verify(mStatsManager, times(1)).registerUsageCallback(
+                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
+    }
+
+    @Test
+    public void testGetMultipathPreference_SnoozedBothQuota() {
+        testGetMultipathPreference(
+                DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
+                OPPORTUNISTIC_QUOTA_UNKNOWN,
+                // 29 days from Apr. 2nd to May 1st
+                POLICY_SNOOZED /* policyWarning */,
+                POLICY_SNOOZED /* policyLimit */,
+                DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
+                2_500_000 /* defaultResSetting */,
+                false /* roaming */);
+
+        // Default global setting should be used: 12 - 7 = 5
+        verify(mStatsManager, times(1)).registerUsageCallback(
+                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any());
+    }
+
+    @Test
+    public void testGetMultipathPreference_SettingChanged() {
+        testGetMultipathPreference(
+                DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
+                OPPORTUNISTIC_QUOTA_UNKNOWN,
+                WARNING_DISABLED,
+                LIMIT_DISABLED,
+                -1 /* defaultGlobalSetting */,
+                DataUnit.MEGABYTES.toBytes(10) /* defaultResSetting */,
+                false /* roaming */);
+
+        verify(mStatsManager, times(1)).registerUsageCallback(
+                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
+
+        // Update setting
+        setDefaultQuotaGlobalSetting(DataUnit.MEGABYTES.toBytes(14));
+        mTracker.mSettingsObserver.onChange(
+                false, Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES));
+
+        // Callback must have been re-registered with new setting
+        verify(mStatsManager, times(1)).unregisterUsageCallback(any());
+        verify(mStatsManager, times(1)).registerUsageCallback(
+                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
+    }
+
+    @Test
+    public void testGetMultipathPreference_ResourceChanged() {
+        testGetMultipathPreference(
+                DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
+                OPPORTUNISTIC_QUOTA_UNKNOWN,
+                WARNING_DISABLED,
+                LIMIT_DISABLED,
+                -1 /* defaultGlobalSetting */,
+                DataUnit.MEGABYTES.toBytes(14) /* defaultResSetting */,
+                false /* roaming */);
+
+        verify(mStatsManager, times(1)).registerUsageCallback(
+                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
+
+        when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes))
+                .thenReturn((int) DataUnit.MEGABYTES.toBytes(16));
+
+        final BroadcastReceiver configChangeReceiver = mConfigChangeReceiverCaptor.getValue();
+        assertNotNull(configChangeReceiver);
+        configChangeReceiver.onReceive(mContext, new Intent());
+
+        // Uses the new setting (16 - 2 = 14MB)
+        verify(mStatsManager, times(1)).registerUsageCallback(
+                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any());
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
new file mode 100644
index 0000000..9b2a638
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
@@ -0,0 +1,555 @@
+/*
+ * 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.connectivity;
+
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.net.ConnectivityManager;
+import android.net.IDnsResolver;
+import android.net.INetd;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.os.Handler;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.ConnectivityService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class Nat464XlatTest {
+
+    static final String BASE_IFACE = "test0";
+    static final String STACKED_IFACE = "v4-test0";
+    static final LinkAddress V6ADDR = new LinkAddress("2001:db8:1::f00/64");
+    static final LinkAddress ADDR = new LinkAddress("192.0.2.5/29");
+    static final String NAT64_PREFIX = "64:ff9b::/96";
+    static final String OTHER_NAT64_PREFIX = "2001:db8:0:64::/96";
+    static final int NETID = 42;
+
+    @Mock ConnectivityService mConnectivity;
+    @Mock IDnsResolver mDnsResolver;
+    @Mock INetd mNetd;
+    @Mock NetworkAgentInfo mNai;
+
+    TestLooper mLooper;
+    Handler mHandler;
+    NetworkAgentConfig mAgentConfig = new NetworkAgentConfig();
+
+    Nat464Xlat makeNat464Xlat(boolean isCellular464XlatEnabled) {
+        return new Nat464Xlat(mNai, mNetd, mDnsResolver, new ConnectivityService.Dependencies()) {
+            @Override protected int getNetId() {
+                return NETID;
+            }
+
+            @Override protected boolean isCellular464XlatEnabled() {
+                return isCellular464XlatEnabled;
+            }
+        };
+    }
+
+    private void markNetworkConnected() {
+        mNai.networkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTED, "", "");
+    }
+
+    private void markNetworkDisconnected() {
+        mNai.networkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED, "", "");
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mLooper = new TestLooper();
+        mHandler = new Handler(mLooper.getLooper());
+
+        MockitoAnnotations.initMocks(this);
+
+        mNai.linkProperties = new LinkProperties();
+        mNai.linkProperties.setInterfaceName(BASE_IFACE);
+        mNai.networkInfo = new NetworkInfo(null);
+        mNai.networkInfo.setType(ConnectivityManager.TYPE_WIFI);
+        mNai.networkCapabilities = new NetworkCapabilities();
+        markNetworkConnected();
+        when(mNai.connService()).thenReturn(mConnectivity);
+        when(mNai.netAgentConfig()).thenReturn(mAgentConfig);
+        when(mNai.handler()).thenReturn(mHandler);
+        final InterfaceConfigurationParcel mConfig = new InterfaceConfigurationParcel();
+        when(mNetd.interfaceGetCfg(eq(STACKED_IFACE))).thenReturn(mConfig);
+        mConfig.ipv4Addr = ADDR.getAddress().getHostAddress();
+        mConfig.prefixLength =  ADDR.getPrefixLength();
+    }
+
+    private void assertRequiresClat(boolean expected, NetworkAgentInfo nai) {
+        Nat464Xlat nat = makeNat464Xlat(true);
+        String msg = String.format("requiresClat expected %b for type=%d state=%s skip=%b "
+                + "nat64Prefix=%s addresses=%s", expected, nai.networkInfo.getType(),
+                nai.networkInfo.getDetailedState(),
+                mAgentConfig.skip464xlat, nai.linkProperties.getNat64Prefix(),
+                nai.linkProperties.getLinkAddresses());
+        assertEquals(msg, expected, nat.requiresClat(nai));
+    }
+
+    private void assertShouldStartClat(boolean expected, NetworkAgentInfo nai) {
+        Nat464Xlat nat = makeNat464Xlat(true);
+        String msg = String.format("shouldStartClat expected %b for type=%d state=%s skip=%b "
+                + "nat64Prefix=%s addresses=%s", expected, nai.networkInfo.getType(),
+                nai.networkInfo.getDetailedState(),
+                mAgentConfig.skip464xlat, nai.linkProperties.getNat64Prefix(),
+                nai.linkProperties.getLinkAddresses());
+        assertEquals(msg, expected, nat.shouldStartClat(nai));
+    }
+
+    @Test
+    public void testRequiresClat() throws Exception {
+        final int[] supportedTypes = {
+            ConnectivityManager.TYPE_MOBILE,
+            ConnectivityManager.TYPE_WIFI,
+            ConnectivityManager.TYPE_ETHERNET,
+        };
+
+        // NetworkInfo doesn't allow setting the State directly, but rather
+        // requires setting DetailedState in order set State as a side-effect.
+        final NetworkInfo.DetailedState[] supportedDetailedStates = {
+            NetworkInfo.DetailedState.CONNECTED,
+            NetworkInfo.DetailedState.SUSPENDED,
+        };
+
+        LinkProperties oldLp = new LinkProperties(mNai.linkProperties);
+        for (int type : supportedTypes) {
+            mNai.networkInfo.setType(type);
+            for (NetworkInfo.DetailedState state : supportedDetailedStates) {
+                mNai.networkInfo.setDetailedState(state, "reason", "extraInfo");
+
+                mNai.linkProperties.setNat64Prefix(new IpPrefix(OTHER_NAT64_PREFIX));
+                assertRequiresClat(false, mNai);
+                assertShouldStartClat(false, mNai);
+
+                mNai.linkProperties.addLinkAddress(new LinkAddress("fc00::1/64"));
+                assertRequiresClat(false, mNai);
+                assertShouldStartClat(false, mNai);
+
+                mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+                assertRequiresClat(true, mNai);
+                assertShouldStartClat(true, mNai);
+
+                mAgentConfig.skip464xlat = true;
+                assertRequiresClat(false, mNai);
+                assertShouldStartClat(false, mNai);
+
+                mAgentConfig.skip464xlat = false;
+                assertRequiresClat(true, mNai);
+                assertShouldStartClat(true, mNai);
+
+                mNai.linkProperties.addLinkAddress(new LinkAddress("192.0.2.2/24"));
+                assertRequiresClat(false, mNai);
+                assertShouldStartClat(false, mNai);
+
+                mNai.linkProperties.removeLinkAddress(new LinkAddress("192.0.2.2/24"));
+                assertRequiresClat(true, mNai);
+                assertShouldStartClat(true, mNai);
+
+                mNai.linkProperties.setNat64Prefix(null);
+                assertRequiresClat(true, mNai);
+                assertShouldStartClat(false, mNai);
+
+                mNai.linkProperties = new LinkProperties(oldLp);
+            }
+        }
+    }
+
+    private void makeClatUnnecessary(boolean dueToDisconnect) {
+        if (dueToDisconnect) {
+            markNetworkDisconnected();
+        } else {
+            mNai.linkProperties.addLinkAddress(ADDR);
+        }
+    }
+
+    private void checkNormalStartAndStop(boolean dueToDisconnect) throws Exception {
+        Nat464Xlat nat = makeNat464Xlat(true);
+        ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
+
+        mNai.linkProperties.addLinkAddress(V6ADDR);
+
+        nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
+
+        // Start clat.
+        nat.start();
+
+        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+
+        // Stacked interface up notification arrives.
+        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
+        mLooper.dispatchNext();
+
+        verify(mNetd).interfaceGetCfg(eq(STACKED_IFACE));
+        verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture());
+        assertFalse(c.getValue().getStackedLinks().isEmpty());
+        assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
+        assertRunning(nat);
+
+        // Stop clat (Network disconnects, IPv4 addr appears, ...).
+        makeClatUnnecessary(dueToDisconnect);
+        nat.stop();
+
+        verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
+        assertTrue(c.getValue().getStackedLinks().isEmpty());
+        assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
+        verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
+        assertIdle(nat);
+
+        // Stacked interface removed notification arrives and is ignored.
+        nat.interfaceRemoved(STACKED_IFACE);
+        mLooper.dispatchNext();
+
+        verifyNoMoreInteractions(mNetd, mConnectivity);
+    }
+
+    @Test
+    public void testNormalStartAndStopDueToDisconnect() throws Exception {
+        checkNormalStartAndStop(true);
+    }
+
+    @Test
+    public void testNormalStartAndStopDueToIpv4Addr() throws Exception {
+        checkNormalStartAndStop(false);
+    }
+
+    private void checkStartStopStart(boolean interfaceRemovedFirst) throws Exception {
+        Nat464Xlat nat = makeNat464Xlat(true);
+        ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
+        InOrder inOrder = inOrder(mNetd, mConnectivity);
+
+        mNai.linkProperties.addLinkAddress(V6ADDR);
+
+        nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
+
+        nat.start();
+
+        inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+
+        // Stacked interface up notification arrives.
+        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
+        mLooper.dispatchNext();
+
+        inOrder.verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture());
+        assertFalse(c.getValue().getStackedLinks().isEmpty());
+        assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
+        assertRunning(nat);
+
+        // ConnectivityService stops clat (Network disconnects, IPv4 addr appears, ...).
+        nat.stop();
+
+        inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE));
+
+        inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
+        assertTrue(c.getValue().getStackedLinks().isEmpty());
+        assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
+        assertIdle(nat);
+
+        if (interfaceRemovedFirst) {
+            // Stacked interface removed notification arrives and is ignored.
+            nat.interfaceRemoved(STACKED_IFACE);
+            mLooper.dispatchNext();
+            nat.interfaceLinkStateChanged(STACKED_IFACE, false);
+            mLooper.dispatchNext();
+        }
+
+        assertTrue(c.getValue().getStackedLinks().isEmpty());
+        assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
+        assertIdle(nat);
+        inOrder.verifyNoMoreInteractions();
+
+        nat.start();
+
+        inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+
+        if (!interfaceRemovedFirst) {
+            // Stacked interface removed notification arrives and is ignored.
+            nat.interfaceRemoved(STACKED_IFACE);
+            mLooper.dispatchNext();
+            nat.interfaceLinkStateChanged(STACKED_IFACE, false);
+            mLooper.dispatchNext();
+        }
+
+        // Stacked interface up notification arrives.
+        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
+        mLooper.dispatchNext();
+
+        inOrder.verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture());
+        assertFalse(c.getValue().getStackedLinks().isEmpty());
+        assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
+        assertRunning(nat);
+
+        // ConnectivityService stops clat again.
+        nat.stop();
+
+        inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE));
+
+        inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
+        assertTrue(c.getValue().getStackedLinks().isEmpty());
+        assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
+        assertIdle(nat);
+
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    public void testStartStopStart() throws Exception {
+        checkStartStopStart(true);
+    }
+
+    @Test
+    public void testStartStopStartBeforeInterfaceRemoved() throws Exception {
+        checkStartStopStart(false);
+    }
+
+    @Test
+    public void testClatdCrashWhileRunning() throws Exception {
+        Nat464Xlat nat = makeNat464Xlat(true);
+        ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
+
+        nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
+
+        nat.start();
+
+        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+
+        // Stacked interface up notification arrives.
+        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
+        mLooper.dispatchNext();
+
+        verify(mNetd).interfaceGetCfg(eq(STACKED_IFACE));
+        verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
+        assertFalse(c.getValue().getStackedLinks().isEmpty());
+        assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
+        assertRunning(nat);
+
+        // Stacked interface removed notification arrives (clatd crashed, ...).
+        nat.interfaceRemoved(STACKED_IFACE);
+        mLooper.dispatchNext();
+
+        verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
+        verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
+        assertTrue(c.getValue().getStackedLinks().isEmpty());
+        assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
+        assertIdle(nat);
+
+        // ConnectivityService stops clat: no-op.
+        nat.stop();
+
+        verifyNoMoreInteractions(mNetd, mConnectivity);
+    }
+
+    private void checkStopBeforeClatdStarts(boolean dueToDisconnect) throws Exception {
+        Nat464Xlat nat = makeNat464Xlat(true);
+
+        mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+
+        nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
+
+        nat.start();
+
+        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+
+        // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
+        makeClatUnnecessary(dueToDisconnect);
+        nat.stop();
+
+        verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
+        assertIdle(nat);
+
+        // In-flight interface up notification arrives: no-op
+        nat.interfaceLinkStateChanged(STACKED_IFACE, true);
+        mLooper.dispatchNext();
+
+        // Interface removed notification arrives after stopClatd() takes effect: no-op.
+        nat.interfaceRemoved(STACKED_IFACE);
+        mLooper.dispatchNext();
+
+        assertIdle(nat);
+
+        verifyNoMoreInteractions(mNetd, mConnectivity);
+    }
+
+    @Test
+    public void testStopDueToDisconnectBeforeClatdStarts() throws Exception {
+        checkStopBeforeClatdStarts(true);
+    }
+
+    @Test
+    public void testStopDueToIpv4AddrBeforeClatdStarts() throws Exception {
+        checkStopBeforeClatdStarts(false);
+    }
+
+    private void checkStopAndClatdNeverStarts(boolean dueToDisconnect) throws Exception {
+        Nat464Xlat nat = makeNat464Xlat(true);
+
+        mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+
+        nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
+
+        nat.start();
+
+        verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+
+        // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
+        makeClatUnnecessary(dueToDisconnect);
+        nat.stop();
+
+        verify(mNetd).clatdStop(eq(BASE_IFACE));
+        verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
+        assertIdle(nat);
+
+        verifyNoMoreInteractions(mNetd, mConnectivity);
+    }
+
+    @Test
+    public void testStopDueToDisconnectAndClatdNeverStarts() throws Exception {
+        checkStopAndClatdNeverStarts(true);
+    }
+
+    @Test
+    public void testStopDueToIpv4AddressAndClatdNeverStarts() throws Exception {
+        checkStopAndClatdNeverStarts(false);
+    }
+
+    @Test
+    public void testNat64PrefixPreference() throws Exception {
+        final IpPrefix prefixFromDns = new IpPrefix(NAT64_PREFIX);
+        final IpPrefix prefixFromRa = new IpPrefix(OTHER_NAT64_PREFIX);
+
+        Nat464Xlat nat = makeNat464Xlat(true);
+
+        final LinkProperties emptyLp = new LinkProperties();
+        LinkProperties fixedupLp;
+
+        fixedupLp = new LinkProperties();
+        nat.setNat64PrefixFromDns(prefixFromDns);
+        nat.fixupLinkProperties(emptyLp, fixedupLp);
+        assertEquals(prefixFromDns, fixedupLp.getNat64Prefix());
+
+        fixedupLp = new LinkProperties();
+        nat.setNat64PrefixFromRa(prefixFromRa);
+        nat.fixupLinkProperties(emptyLp, fixedupLp);
+        assertEquals(prefixFromRa, fixedupLp.getNat64Prefix());
+
+        fixedupLp = new LinkProperties();
+        nat.setNat64PrefixFromRa(null);
+        nat.fixupLinkProperties(emptyLp, fixedupLp);
+        assertEquals(prefixFromDns, fixedupLp.getNat64Prefix());
+
+        fixedupLp = new LinkProperties();
+        nat.setNat64PrefixFromRa(prefixFromRa);
+        nat.fixupLinkProperties(emptyLp, fixedupLp);
+        assertEquals(prefixFromRa, fixedupLp.getNat64Prefix());
+
+        fixedupLp = new LinkProperties();
+        nat.setNat64PrefixFromDns(null);
+        nat.fixupLinkProperties(emptyLp, fixedupLp);
+        assertEquals(prefixFromRa, fixedupLp.getNat64Prefix());
+
+        fixedupLp = new LinkProperties();
+        nat.setNat64PrefixFromRa(null);
+        nat.fixupLinkProperties(emptyLp, fixedupLp);
+        assertEquals(null, fixedupLp.getNat64Prefix());
+    }
+
+    private void checkClatDisabledOnCellular(boolean onCellular) throws Exception {
+        // Disable 464xlat on cellular networks.
+        Nat464Xlat nat = makeNat464Xlat(false);
+        mNai.linkProperties.addLinkAddress(V6ADDR);
+        mNai.networkCapabilities.setTransportType(TRANSPORT_CELLULAR, onCellular);
+        nat.update();
+
+        final IpPrefix nat64Prefix = new IpPrefix(NAT64_PREFIX);
+        if (onCellular) {
+            // Prefix discovery is never started.
+            verify(mDnsResolver, never()).startPrefix64Discovery(eq(NETID));
+            assertIdle(nat);
+
+            // If a NAT64 prefix comes in from an RA, clat is not started either.
+            mNai.linkProperties.setNat64Prefix(nat64Prefix);
+            nat.setNat64PrefixFromRa(nat64Prefix);
+            nat.update();
+            verify(mNetd, never()).clatdStart(anyString(), anyString());
+            assertIdle(nat);
+        } else {
+            // Prefix discovery is started.
+            verify(mDnsResolver).startPrefix64Discovery(eq(NETID));
+            assertIdle(nat);
+
+            // If a NAT64 prefix comes in from an RA, clat is started.
+            mNai.linkProperties.setNat64Prefix(nat64Prefix);
+            nat.setNat64PrefixFromRa(nat64Prefix);
+            nat.update();
+            verify(mNetd).clatdStart(BASE_IFACE, NAT64_PREFIX);
+            assertStarting(nat);
+        }
+    }
+
+    @Test
+    public void testClatDisabledOnCellular() throws Exception {
+        checkClatDisabledOnCellular(true);
+    }
+
+    @Test
+    public void testClatDisabledOnNonCellular() throws Exception {
+        checkClatDisabledOnCellular(false);
+    }
+
+    static void assertIdle(Nat464Xlat nat) {
+        assertTrue("Nat464Xlat was not IDLE", !nat.isStarted());
+    }
+
+    static void assertStarting(Nat464Xlat nat) {
+        assertTrue("Nat464Xlat was not STARTING", nat.isStarting());
+    }
+
+    static void assertRunning(Nat464Xlat nat) {
+        assertTrue("Nat464Xlat was not RUNNING", nat.isRunning());
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
new file mode 100644
index 0000000..50aaaee
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
@@ -0,0 +1,554 @@
+/*
+ * 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.connectivity;
+
+import static android.net.metrics.INetdEventListener.EVENT_GETADDRINFO;
+import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME;
+
+import static com.android.testutils.MiscAsserts.assertStringContains;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.system.OsConstants;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Base64;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
+import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetdEventListenerServiceTest {
+    private static final String EXAMPLE_IPV4 = "192.0.2.1";
+    private static final String EXAMPLE_IPV6 = "2001:db8:1200::2:1";
+
+    private static final byte[] MAC_ADDR =
+            {(byte)0x84, (byte)0xc9, (byte)0xb2, (byte)0x6a, (byte)0xed, (byte)0x4b};
+
+    NetdEventListenerService mService;
+    ConnectivityManager mCm;
+    private static final NetworkCapabilities CAPABILITIES_WIFI = new NetworkCapabilities.Builder()
+            .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+            .build();
+    private static final NetworkCapabilities CAPABILITIES_CELL = new NetworkCapabilities.Builder()
+            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+            .build();
+
+    @Before
+    public void setUp() {
+        mCm = mock(ConnectivityManager.class);
+        mService = new NetdEventListenerService(mCm);
+    }
+
+    @Test
+    public void testWakeupEventLogging() throws Exception {
+        final int BUFFER_LENGTH = NetdEventListenerService.WAKEUP_EVENT_BUFFER_LENGTH;
+        final long now = System.currentTimeMillis();
+        final String iface = "wlan0";
+        final byte[] mac = MAC_ADDR;
+        final String srcIp = "192.168.2.1";
+        final String dstIp = "192.168.2.23";
+        final String srcIp6 = "2001:db8:4:fd00:a585:13d1:6a23:4fb4";
+        final String dstIp6 = "2001:db8:4006:807::200a";
+        final int sport = 2356;
+        final int dport = 13489;
+
+        final int v4 = 0x800;
+        final int v6 = 0x86dd;
+        final int tcp = 6;
+        final int udp = 17;
+        final int icmp6 = 58;
+
+        // Baseline without any event
+        String[] baseline = listNetdEvent();
+
+        int[] uids = {10001, 10002, 10004, 1000, 10052, 10023, 10002, 10123, 10004};
+        wakeupEvent(iface, uids[0], v4, tcp, mac, srcIp, dstIp, sport, dport, now);
+        wakeupEvent(iface, uids[1], v6, udp, mac, srcIp6, dstIp6, sport, dport, now);
+        wakeupEvent(iface, uids[2], v6, udp, mac, srcIp6, dstIp6, sport, dport, now);
+        wakeupEvent(iface, uids[3], v4, icmp6, mac, srcIp, dstIp, sport, dport, now);
+        wakeupEvent(iface, uids[4], v6, tcp, mac, srcIp6, dstIp6, sport, dport, now);
+        wakeupEvent(iface, uids[5], v4, tcp, mac, srcIp, dstIp, sport, dport, now);
+        wakeupEvent(iface, uids[6], v6, udp, mac, srcIp6, dstIp6, sport, dport, now);
+        wakeupEvent(iface, uids[7], v6, tcp, mac, srcIp6, dstIp6, sport, dport, now);
+        wakeupEvent(iface, uids[8], v6, udp, mac, srcIp6, dstIp6, sport, dport, now);
+
+        String[] events2 = remove(listNetdEvent(), baseline);
+        int expectedLength2 = uids.length + 1; // +1 for the WakeupStats line
+        assertEquals(expectedLength2, events2.length);
+        assertStringContains(events2[0], "WakeupStats");
+        assertStringContains(events2[0], "wlan0");
+        assertStringContains(events2[0], "0x800");
+        assertStringContains(events2[0], "0x86dd");
+        for (int i = 0; i < uids.length; i++) {
+            String got = events2[i+1];
+            assertStringContains(got, "WakeupEvent");
+            assertStringContains(got, "wlan0");
+            assertStringContains(got, "uid: " + uids[i]);
+        }
+
+        int uid = 20000;
+        for (int i = 0; i < BUFFER_LENGTH * 2; i++) {
+            long ts = now + 10;
+            wakeupEvent(iface, uid, 0x800, 6, mac, srcIp, dstIp, 23, 24, ts);
+        }
+
+        String[] events3 = remove(listNetdEvent(), baseline);
+        int expectedLength3 = BUFFER_LENGTH + 1; // +1 for the WakeupStats line
+        assertEquals(expectedLength3, events3.length);
+        assertStringContains(events2[0], "WakeupStats");
+        assertStringContains(events2[0], "wlan0");
+        for (int i = 1; i < expectedLength3; i++) {
+            String got = events3[i];
+            assertStringContains(got, "WakeupEvent");
+            assertStringContains(got, "wlan0");
+            assertStringContains(got, "uid: " + uid);
+        }
+
+        uid = 45678;
+        wakeupEvent(iface, uid, 0x800, 6, mac, srcIp, dstIp, 23, 24, now);
+
+        String[] events4 = remove(listNetdEvent(), baseline);
+        String lastEvent = events4[events4.length - 1];
+        assertStringContains(lastEvent, "WakeupEvent");
+        assertStringContains(lastEvent, "wlan0");
+        assertStringContains(lastEvent, "uid: " + uid);
+    }
+
+    @Test
+    public void testWakeupStatsLogging() throws Exception {
+        final byte[] mac = MAC_ADDR;
+        final String srcIp = "192.168.2.1";
+        final String dstIp = "192.168.2.23";
+        final String srcIp6 = "2401:fa00:4:fd00:a585:13d1:6a23:4fb4";
+        final String dstIp6 = "2404:6800:4006:807::200a";
+        final int sport = 2356;
+        final int dport = 13489;
+        final long now = 1001L;
+
+        final int v4 = 0x800;
+        final int v6 = 0x86dd;
+        final int tcp = 6;
+        final int udp = 17;
+        final int icmp6 = 58;
+
+        wakeupEvent("wlan0", 1000, v4, tcp, mac, srcIp, dstIp, sport, dport, now);
+        wakeupEvent("rmnet0", 10123, v4, tcp, mac, srcIp, dstIp, sport, dport, now);
+        wakeupEvent("wlan0", 1000, v4, udp, mac, srcIp, dstIp, sport, dport, now);
+        wakeupEvent("rmnet0", 10008, v4, tcp, mac, srcIp, dstIp, sport, dport, now);
+        wakeupEvent("wlan0", -1, v6, icmp6, mac, srcIp6, dstIp6, sport, dport, now);
+        wakeupEvent("wlan0", 10008, v4, tcp, mac, srcIp, dstIp, sport, dport, now);
+        wakeupEvent("rmnet0", 1000, v4, tcp, mac, srcIp, dstIp, sport, dport, now);
+        wakeupEvent("wlan0", 10004, v4, udp, mac, srcIp, dstIp, sport, dport, now);
+        wakeupEvent("wlan0", 1000, v6, tcp, mac, srcIp6, dstIp6, sport, dport, now);
+        wakeupEvent("wlan0", 0, v6, udp, mac, srcIp6, dstIp6, sport, dport, now);
+        wakeupEvent("wlan0", -1, v6, icmp6, mac, srcIp6, dstIp6, sport, dport, now);
+        wakeupEvent("rmnet0", 10052, v4, tcp, mac, srcIp, dstIp, sport, dport, now);
+        wakeupEvent("wlan0", 0, v6, udp, mac, srcIp6, dstIp6, sport, dport, now);
+        wakeupEvent("rmnet0", 1000, v6, tcp, mac, srcIp6, dstIp6, sport, dport, now);
+        wakeupEvent("wlan0", 1010, v4, udp, mac, srcIp, dstIp, sport, dport, now);
+
+        String got = flushStatistics();
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 2",
+                "  network_id: 0",
+                "  time_ms: 0",
+                "  transports: 0",
+                "  wakeup_stats <",
+                "    application_wakeups: 3",
+                "    duration_sec: 0",
+                "    ethertype_counts <",
+                "      key: 2048",
+                "      value: 4",
+                "    >",
+                "    ethertype_counts <",
+                "      key: 34525",
+                "      value: 1",
+                "    >",
+                "    ip_next_header_counts <",
+                "      key: 6",
+                "      value: 5",
+                "    >",
+                "    l2_broadcast_count: 0",
+                "    l2_multicast_count: 0",
+                "    l2_unicast_count: 5",
+                "    no_uid_wakeups: 0",
+                "    non_application_wakeups: 0",
+                "    root_wakeups: 0",
+                "    system_wakeups: 2",
+                "    total_wakeups: 5",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 0",
+                "  time_ms: 0",
+                "  transports: 0",
+                "  wakeup_stats <",
+                "    application_wakeups: 2",
+                "    duration_sec: 0",
+                "    ethertype_counts <",
+                "      key: 2048",
+                "      value: 5",
+                "    >",
+                "    ethertype_counts <",
+                "      key: 34525",
+                "      value: 5",
+                "    >",
+                "    ip_next_header_counts <",
+                "      key: 6",
+                "      value: 3",
+                "    >",
+                "    ip_next_header_counts <",
+                "      key: 17",
+                "      value: 5",
+                "    >",
+                "    ip_next_header_counts <",
+                "      key: 58",
+                "      value: 2",
+                "    >",
+                "    l2_broadcast_count: 0",
+                "    l2_multicast_count: 0",
+                "    l2_unicast_count: 10",
+                "    no_uid_wakeups: 2",
+                "    non_application_wakeups: 1",
+                "    root_wakeups: 2",
+                "    system_wakeups: 3",
+                "    total_wakeups: 10",
+                "  >",
+                ">",
+                "version: 2\n");
+        assertEquals(want, got);
+    }
+
+    @Test
+    public void testDnsLogging() throws Exception {
+        asyncDump(100);
+
+        dnsEvent(100, EVENT_GETADDRINFO, 0, 3456);
+        dnsEvent(100, EVENT_GETADDRINFO, 0, 267);
+        dnsEvent(100, EVENT_GETHOSTBYNAME, 22, 1230);
+        dnsEvent(100, EVENT_GETADDRINFO, 3, 45);
+        dnsEvent(100, EVENT_GETADDRINFO, 1, 2111);
+        dnsEvent(100, EVENT_GETADDRINFO, 0, 450);
+        dnsEvent(100, EVENT_GETHOSTBYNAME, 200, 638);
+        dnsEvent(100, EVENT_GETHOSTBYNAME, 178, 1300);
+        dnsEvent(101, EVENT_GETADDRINFO, 0, 56);
+        dnsEvent(101, EVENT_GETADDRINFO, 0, 78);
+        dnsEvent(101, EVENT_GETADDRINFO, 0, 14);
+        dnsEvent(101, EVENT_GETHOSTBYNAME, 0, 56);
+        dnsEvent(101, EVENT_GETADDRINFO, 0, 78);
+        dnsEvent(101, EVENT_GETADDRINFO, 0, 14);
+
+        String got = flushStatistics();
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 100",
+                "  time_ms: 0",
+                "  transports: 2",
+                "  dns_lookup_batch <",
+                "    event_types: 1",
+                "    event_types: 1",
+                "    event_types: 2",
+                "    event_types: 1",
+                "    event_types: 1",
+                "    event_types: 1",
+                "    event_types: 2",
+                "    event_types: 2",
+                "    getaddrinfo_error_count: 0",
+                "    getaddrinfo_query_count: 0",
+                "    gethostbyname_error_count: 0",
+                "    gethostbyname_query_count: 0",
+                "    latencies_ms: 3456",
+                "    latencies_ms: 267",
+                "    latencies_ms: 1230",
+                "    latencies_ms: 45",
+                "    latencies_ms: 2111",
+                "    latencies_ms: 450",
+                "    latencies_ms: 638",
+                "    latencies_ms: 1300",
+                "    return_codes: 0",
+                "    return_codes: 0",
+                "    return_codes: 22",
+                "    return_codes: 3",
+                "    return_codes: 1",
+                "    return_codes: 0",
+                "    return_codes: 200",
+                "    return_codes: 178",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 2",
+                "  network_id: 101",
+                "  time_ms: 0",
+                "  transports: 1",
+                "  dns_lookup_batch <",
+                "    event_types: 1",
+                "    event_types: 1",
+                "    event_types: 1",
+                "    event_types: 2",
+                "    event_types: 1",
+                "    event_types: 1",
+                "    getaddrinfo_error_count: 0",
+                "    getaddrinfo_query_count: 0",
+                "    gethostbyname_error_count: 0",
+                "    gethostbyname_query_count: 0",
+                "    latencies_ms: 56",
+                "    latencies_ms: 78",
+                "    latencies_ms: 14",
+                "    latencies_ms: 56",
+                "    latencies_ms: 78",
+                "    latencies_ms: 14",
+                "    return_codes: 0",
+                "    return_codes: 0",
+                "    return_codes: 0",
+                "    return_codes: 0",
+                "    return_codes: 0",
+                "    return_codes: 0",
+                "  >",
+                ">",
+                "version: 2\n");
+        assertEquals(want, got);
+    }
+
+    @Test
+    public void testConnectLogging() throws Exception {
+        asyncDump(100);
+
+        final int OK = 0;
+        Thread[] logActions = {
+            // ignored
+            connectEventAction(100, OsConstants.EALREADY, 0, EXAMPLE_IPV4),
+            connectEventAction(100, OsConstants.EALREADY, 0, EXAMPLE_IPV6),
+            connectEventAction(100, OsConstants.EINPROGRESS, 0, EXAMPLE_IPV4),
+            connectEventAction(101, OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6),
+            connectEventAction(101, OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6),
+            // valid latencies
+            connectEventAction(100, OK, 110, EXAMPLE_IPV4),
+            connectEventAction(100, OK, 23, EXAMPLE_IPV4),
+            connectEventAction(100, OK, 45, EXAMPLE_IPV4),
+            connectEventAction(101, OK, 56, EXAMPLE_IPV4),
+            connectEventAction(101, OK, 523, EXAMPLE_IPV6),
+            connectEventAction(101, OK, 214, EXAMPLE_IPV6),
+            connectEventAction(101, OK, 67, EXAMPLE_IPV6),
+            // errors
+            connectEventAction(100, OsConstants.EPERM, 0, EXAMPLE_IPV4),
+            connectEventAction(101, OsConstants.EPERM, 0, EXAMPLE_IPV4),
+            connectEventAction(100, OsConstants.EAGAIN, 0, EXAMPLE_IPV4),
+            connectEventAction(100, OsConstants.EACCES, 0, EXAMPLE_IPV4),
+            connectEventAction(101, OsConstants.EACCES, 0, EXAMPLE_IPV4),
+            connectEventAction(101, OsConstants.EACCES, 0, EXAMPLE_IPV6),
+            connectEventAction(100, OsConstants.EADDRINUSE, 0, EXAMPLE_IPV4),
+            connectEventAction(101, OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV4),
+            connectEventAction(100, OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV6),
+            connectEventAction(100, OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV6),
+            connectEventAction(101, OsConstants.ECONNREFUSED, 0, EXAMPLE_IPV4),
+        };
+
+        for (Thread t : logActions) {
+            t.start();
+        }
+        for (Thread t : logActions) {
+            t.join();
+        }
+
+        String got = flushStatistics();
+        String want = String.join("\n",
+                "dropped_events: 0",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 4",
+                "  network_id: 100",
+                "  time_ms: 0",
+                "  transports: 2",
+                "  connect_statistics <",
+                "    connect_blocking_count: 3",
+                "    connect_count: 6",
+                "    errnos_counters <",
+                "      key: 1",
+                "      value: 1",
+                "    >",
+                "    errnos_counters <",
+                "      key: 11",
+                "      value: 1",
+                "    >",
+                "    errnos_counters <",
+                "      key: 13",
+                "      value: 1",
+                "    >",
+                "    errnos_counters <",
+                "      key: 98",
+                "      value: 1",
+                "    >",
+                "    errnos_counters <",
+                "      key: 110",
+                "      value: 2",
+                "    >",
+                "    ipv6_addr_count: 1",
+                "    latencies_ms: 23",
+                "    latencies_ms: 45",
+                "    latencies_ms: 110",
+                "  >",
+                ">",
+                "events <",
+                "  if_name: \"\"",
+                "  link_layer: 2",
+                "  network_id: 101",
+                "  time_ms: 0",
+                "  transports: 1",
+                "  connect_statistics <",
+                "    connect_blocking_count: 4",
+                "    connect_count: 6",
+                "    errnos_counters <",
+                "      key: 1",
+                "      value: 1",
+                "    >",
+                "    errnos_counters <",
+                "      key: 13",
+                "      value: 2",
+                "    >",
+                "    errnos_counters <",
+                "      key: 110",
+                "      value: 1",
+                "    >",
+                "    errnos_counters <",
+                "      key: 111",
+                "      value: 1",
+                "    >",
+                "    ipv6_addr_count: 5",
+                "    latencies_ms: 56",
+                "    latencies_ms: 67",
+                "    latencies_ms: 214",
+                "    latencies_ms: 523",
+                "  >",
+                ">",
+                "version: 2\n");
+        assertEquals(want, got);
+    }
+
+    private void setCapabilities(int netId) {
+        final ArgumentCaptor<ConnectivityManager.NetworkCallback> networkCallback =
+                ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
+        verify(mCm).registerNetworkCallback(any(), networkCallback.capture());
+        networkCallback.getValue().onCapabilitiesChanged(new Network(netId),
+                netId == 100 ? CAPABILITIES_WIFI : CAPABILITIES_CELL);
+    }
+
+    Thread connectEventAction(int netId, int error, int latencyMs, String ipAddr) {
+        setCapabilities(netId);
+        return new Thread(() -> {
+            try {
+                mService.onConnectEvent(netId, error, latencyMs, ipAddr, 80, 1);
+            } catch (Exception e) {
+                fail(e.toString());
+            }
+        });
+    }
+
+    void dnsEvent(int netId, int type, int result, int latency) throws Exception {
+        setCapabilities(netId);
+        mService.onDnsEvent(netId, type, result, latency, "", null, 0, 0);
+    }
+
+    void wakeupEvent(String iface, int uid, int ether, int ip, byte[] mac, String srcIp,
+            String dstIp, int sport, int dport, long now) throws Exception {
+        String prefix = NetdEventListenerService.WAKEUP_EVENT_IFACE_PREFIX + iface;
+        mService.onWakeupEvent(prefix, uid, ether, ip, mac, srcIp, dstIp, sport, dport, now);
+    }
+
+    void asyncDump(long durationMs) throws Exception {
+        final long stop = System.currentTimeMillis() + durationMs;
+        final PrintWriter pw = new PrintWriter(new FileOutputStream("/dev/null"));
+        new Thread(() -> {
+            while (System.currentTimeMillis() < stop) {
+                mService.list(pw);
+            }
+        }).start();
+    }
+
+    // TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto.
+    String flushStatistics() throws Exception {
+        IpConnectivityMetrics metricsService =
+                new IpConnectivityMetrics(mock(Context.class), (ctx) -> 2000);
+        metricsService.mNetdListener = mService;
+
+        StringWriter buffer = new StringWriter();
+        PrintWriter writer = new PrintWriter(buffer);
+        metricsService.impl.dump(null, writer, new String[]{"flush"});
+        byte[] bytes = Base64.decode(buffer.toString(), Base64.DEFAULT);
+        IpConnectivityLog log = IpConnectivityLog.parseFrom(bytes);
+        for (IpConnectivityEvent ev : log.events) {
+            if (ev.getConnectStatistics() == null) {
+                continue;
+            }
+            // Sort repeated fields of connect() events arriving in non-deterministic order.
+            Arrays.sort(ev.getConnectStatistics().latenciesMs);
+            Arrays.sort(ev.getConnectStatistics().errnosCounters,
+                    Comparator.comparingInt((p) -> p.key));
+        }
+        return log.toString();
+    }
+
+    String[] listNetdEvent() throws Exception {
+        StringWriter buffer = new StringWriter();
+        PrintWriter writer = new PrintWriter(buffer);
+        mService.list(writer);
+        return buffer.toString().split("\\n");
+    }
+
+    static <T> T[] remove(T[] array, T[] filtered) {
+        List<T> c = Arrays.asList(filtered);
+        int next = 0;
+        for (int i = 0; i < array.length; i++) {
+            if (c.contains(array[i])) {
+                continue;
+            }
+            array[next++] = array[i];
+        }
+        return Arrays.copyOf(array, next);
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
new file mode 100644
index 0000000..3adf08c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -0,0 +1,295 @@
+/*
+ * 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.connectivity;
+
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.*;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.clearInvocations;
+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;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.ConnectivityResources;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.os.UserHandle;
+import android.telephony.TelephonyManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.AdditionalAnswers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkNotificationManagerTest {
+
+    private static final String TEST_SSID = "Test SSID";
+    private static final String TEST_EXTRA_INFO = "extra";
+    static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities();
+    static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities();
+    static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities();
+    static {
+        CELL_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        CELL_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+
+        WIFI_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
+        WIFI_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        WIFI_CAPABILITIES.setSSID(TEST_SSID);
+
+        // Set the underyling network to wifi.
+        VPN_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
+        VPN_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_VPN);
+        VPN_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        VPN_CAPABILITIES.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
+    }
+
+    @Mock Context mCtx;
+    @Mock Resources mResources;
+    @Mock PackageManager mPm;
+    @Mock TelephonyManager mTelephonyManager;
+    @Mock NotificationManager mNotificationManager;
+    @Mock NetworkAgentInfo mWifiNai;
+    @Mock NetworkAgentInfo mCellNai;
+    @Mock NetworkAgentInfo mVpnNai;
+    @Mock NetworkInfo mNetworkInfo;
+    ArgumentCaptor<Notification> mCaptor;
+
+    NetworkNotificationManager mManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mCaptor = ArgumentCaptor.forClass(Notification.class);
+        mWifiNai.networkCapabilities = WIFI_CAPABILITIES;
+        mWifiNai.networkInfo = mNetworkInfo;
+        mCellNai.networkCapabilities = CELL_CAPABILITIES;
+        mCellNai.networkInfo = mNetworkInfo;
+        mVpnNai.networkCapabilities = VPN_CAPABILITIES;
+        mVpnNai.networkInfo = mNetworkInfo;
+        doReturn(true).when(mVpnNai).isVPN();
+        when(mCtx.getResources()).thenReturn(mResources);
+        when(mCtx.getPackageManager()).thenReturn(mPm);
+        when(mCtx.getApplicationInfo()).thenReturn(new ApplicationInfo());
+        final Context asUserCtx = mock(Context.class, AdditionalAnswers.delegatesTo(mCtx));
+        doReturn(UserHandle.ALL).when(asUserCtx).getUser();
+        when(mCtx.createContextAsUser(eq(UserHandle.ALL), anyInt())).thenReturn(asUserCtx);
+        when(mCtx.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
+                .thenReturn(mNotificationManager);
+        when(mNetworkInfo.getExtraInfo()).thenReturn(TEST_EXTRA_INFO);
+        ConnectivityResources.setResourcesContextForTest(mCtx);
+        when(mResources.getColor(anyInt(), any())).thenReturn(0xFF607D8B);
+
+        // Come up with some credible-looking transport names. The actual values do not matter.
+        String[] transportNames = new String[NetworkCapabilities.MAX_TRANSPORT + 1];
+        for (int transport = 0; transport <= NetworkCapabilities.MAX_TRANSPORT; transport++) {
+            transportNames[transport] = NetworkCapabilities.transportNameOf(transport);
+        }
+        when(mResources.getStringArray(R.array.network_switch_type_name))
+            .thenReturn(transportNames);
+
+        mManager = new NetworkNotificationManager(mCtx, mTelephonyManager);
+    }
+
+    @After
+    public void tearDown() {
+        ConnectivityResources.setResourcesContextForTest(null);
+    }
+
+    private void verifyTitleByNetwork(final int id, final NetworkAgentInfo nai, final int title) {
+        final String tag = NetworkNotificationManager.tagFor(id);
+        mManager.showNotification(id, PRIVATE_DNS_BROKEN, nai, null, null, true);
+        verify(mNotificationManager, times(1))
+                .notify(eq(tag), eq(PRIVATE_DNS_BROKEN.eventId), any());
+        final int transportType = NetworkNotificationManager.approximateTransportType(nai);
+        if (transportType == NetworkCapabilities.TRANSPORT_WIFI) {
+            verify(mResources, times(1)).getString(eq(title), eq(TEST_EXTRA_INFO));
+        } else {
+            verify(mResources, times(1)).getString(title);
+        }
+        verify(mResources, times(1)).getString(eq(R.string.private_dns_broken_detailed));
+    }
+
+    @Test
+    public void testTitleOfPrivateDnsBroken() {
+        // Test the title of mobile data.
+        verifyTitleByNetwork(100, mCellNai, R.string.mobile_no_internet);
+        clearInvocations(mResources);
+
+        // Test the title of wifi.
+        verifyTitleByNetwork(101, mWifiNai, R.string.wifi_no_internet);
+        clearInvocations(mResources);
+
+        // Test the title of other networks.
+        verifyTitleByNetwork(102, mVpnNai, R.string.other_networks_no_internet);
+        clearInvocations(mResources);
+    }
+
+    @Test
+    public void testNotificationsShownAndCleared() {
+        final int NETWORK_ID_BASE = 100;
+        List<NotificationType> types = Arrays.asList(NotificationType.values());
+        List<Integer> ids = new ArrayList<>(types.size());
+        for (int i = 0; i < types.size(); i++) {
+            ids.add(NETWORK_ID_BASE + i);
+        }
+        Collections.shuffle(ids);
+        Collections.shuffle(types);
+
+        for (int i = 0; i < ids.size(); i++) {
+            mManager.showNotification(ids.get(i), types.get(i), mWifiNai, mCellNai, null, false);
+        }
+
+        List<Integer> idsToClear = new ArrayList<>(ids);
+        Collections.shuffle(idsToClear);
+        for (int i = 0; i < ids.size(); i++) {
+            mManager.clearNotification(idsToClear.get(i));
+        }
+
+        for (int i = 0; i < ids.size(); i++) {
+            final int id = ids.get(i);
+            final int eventId = types.get(i).eventId;
+            final String tag = NetworkNotificationManager.tagFor(id);
+            verify(mNotificationManager, times(1)).notify(eq(tag), eq(eventId), any());
+            verify(mNotificationManager, times(1)).cancel(eq(tag), eq(eventId));
+        }
+    }
+
+    @Test
+    @Ignore
+    // Ignored because the code under test calls Log.wtf, which crashes the tests on eng builds.
+    // TODO: re-enable after fixing this (e.g., turn Log.wtf into exceptions that this test catches)
+    public void testNoInternetNotificationsNotShownForCellular() {
+        mManager.showNotification(100, NO_INTERNET, mCellNai, mWifiNai, null, false);
+        mManager.showNotification(101, LOST_INTERNET, mCellNai, mWifiNai, null, false);
+
+        verify(mNotificationManager, never()).notify(any(), anyInt(), any());
+
+        mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false);
+
+        final int eventId = NO_INTERNET.eventId;
+        final String tag = NetworkNotificationManager.tagFor(102);
+        verify(mNotificationManager, times(1)).notify(eq(tag), eq(eventId), any());
+    }
+
+    @Test
+    public void testNotificationsNotShownIfNoInternetCapability() {
+        mWifiNai.networkCapabilities = new NetworkCapabilities();
+        mWifiNai.networkCapabilities .addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
+        mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false);
+        mManager.showNotification(103, LOST_INTERNET, mWifiNai, mCellNai, null, false);
+        mManager.showNotification(104, NETWORK_SWITCH, mWifiNai, mCellNai, null, false);
+
+        verify(mNotificationManager, never()).notify(any(), anyInt(), any());
+    }
+
+    @Test
+    public void testDuplicatedNotificationsNoInternetThenSignIn() {
+        final int id = 101;
+        final String tag = NetworkNotificationManager.tagFor(id);
+
+        // Show first NO_INTERNET
+        mManager.showNotification(id, NO_INTERNET, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, times(1)).notify(eq(tag), eq(NO_INTERNET.eventId), any());
+
+        // Captive portal detection triggers SIGN_IN a bit later, clearing the previous NO_INTERNET
+        mManager.showNotification(id, SIGN_IN, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, times(1)).cancel(eq(tag), eq(NO_INTERNET.eventId));
+        verify(mNotificationManager, times(1)).notify(eq(tag), eq(SIGN_IN.eventId), any());
+
+        // Network disconnects
+        mManager.clearNotification(id);
+        verify(mNotificationManager, times(1)).cancel(eq(tag), eq(SIGN_IN.eventId));
+    }
+
+    @Test
+    public void testDuplicatedNotificationsSignInThenNoInternet() {
+        final int id = 101;
+        final String tag = NetworkNotificationManager.tagFor(id);
+
+        // Show first SIGN_IN
+        mManager.showNotification(id, SIGN_IN, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, times(1)).notify(eq(tag), eq(SIGN_IN.eventId), any());
+        reset(mNotificationManager);
+
+        // NO_INTERNET arrives after, but is ignored.
+        mManager.showNotification(id, NO_INTERNET, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, never()).cancel(any(), anyInt());
+        verify(mNotificationManager, never()).notify(any(), anyInt(), any());
+
+        // Network disconnects
+        mManager.clearNotification(id);
+        verify(mNotificationManager, times(1)).cancel(eq(tag), eq(SIGN_IN.eventId));
+    }
+
+    @Test
+    public void testClearNotificationByType() {
+        final int id = 101;
+        final String tag = NetworkNotificationManager.tagFor(id);
+
+        // clearNotification(int id, NotificationType notifyType) will check if given type is equal
+        // to previous type or not. If they are equal then clear the notification; if they are not
+        // equal then return.
+        mManager.showNotification(id, NO_INTERNET, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, times(1)).notify(eq(tag), eq(NO_INTERNET.eventId), any());
+
+        // Previous notification is NO_INTERNET and given type is NO_INTERNET too. The notification
+        // should be cleared.
+        mManager.clearNotification(id, NO_INTERNET);
+        verify(mNotificationManager, times(1)).cancel(eq(tag), eq(NO_INTERNET.eventId));
+
+        // SIGN_IN is popped-up.
+        mManager.showNotification(id, SIGN_IN, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, times(1)).notify(eq(tag), eq(SIGN_IN.eventId), any());
+
+        // The notification type is not matching previous one, PARTIAL_CONNECTIVITY won't be
+        // cleared.
+        mManager.clearNotification(id, PARTIAL_CONNECTIVITY);
+        verify(mNotificationManager, never()).cancel(eq(tag), eq(PARTIAL_CONNECTIVITY.eventId));
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt
new file mode 100644
index 0000000..409f8c3
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt
@@ -0,0 +1,71 @@
+/*
+ * 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 android.net.INetworkOfferCallback
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.NetworkScore.KEEP_CONNECTED_NONE
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+const val POLICY_NONE = 0L
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NetworkOfferTest {
+    val mockCallback = mock(INetworkOfferCallback::class.java)
+
+    @Test
+    fun testOfferNeededUnneeded() {
+        val score = FullScore(50, POLICY_NONE, KEEP_CONNECTED_NONE)
+        val offer = NetworkOffer(score, NetworkCapabilities.Builder().build(), mockCallback,
+                1 /* providerId */)
+        val request1 = mock(NetworkRequest::class.java)
+        val request2 = mock(NetworkRequest::class.java)
+        offer.onNetworkNeeded(request1)
+        verify(mockCallback).onNetworkNeeded(eq(request1))
+        assertTrue(offer.neededFor(request1))
+        assertFalse(offer.neededFor(request2))
+
+        offer.onNetworkNeeded(request2)
+        verify(mockCallback).onNetworkNeeded(eq(request2))
+        assertTrue(offer.neededFor(request1))
+        assertTrue(offer.neededFor(request2))
+
+        // Note that the framework never calls onNetworkNeeded multiple times with the same
+        // request without calling onNetworkUnneeded first. It would be incorrect usage and the
+        // behavior would be undefined, so there is nothing to test.
+
+        offer.onNetworkUnneeded(request1)
+        verify(mockCallback).onNetworkUnneeded(eq(request1))
+        assertFalse(offer.neededFor(request1))
+        assertTrue(offer.neededFor(request2))
+
+        offer.onNetworkUnneeded(request2)
+        verify(mockCallback).onNetworkUnneeded(eq(request2))
+        assertFalse(offer.neededFor(request1))
+        assertFalse(offer.neededFor(request2))
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
new file mode 100644
index 0000000..551b94c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.connectivity
+
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.NetworkScore.KEEP_CONNECTED_NONE
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class NetworkRankerTest {
+    private val ranker = NetworkRanker()
+
+    private fun makeNai(satisfy: Boolean, legacyScore: Int) =
+            mock(NetworkAgentInfo::class.java).also {
+                doReturn(satisfy).`when`(it).satisfies(any())
+                val fs = FullScore(legacyScore, 0 /* policies */, KEEP_CONNECTED_NONE)
+                doReturn(fs).`when`(it).getScore()
+                val nc = NetworkCapabilities.Builder().build()
+                doReturn(nc).`when`(it).getCapsNoCopy()
+            }
+
+    @Test
+    fun testGetBestNetwork() {
+        val scores = listOf(20, 50, 90, 60, 23, 68)
+        val nais = scores.map { makeNai(true, it) }
+        val bestNetwork = nais[2] // The one with the top score
+        val someRequest = mock(NetworkRequest::class.java)
+        assertEquals(bestNetwork, ranker.getBestNetwork(someRequest, nais, bestNetwork))
+    }
+
+    @Test
+    fun testIgnoreNonSatisfying() {
+        val nais = listOf(makeNai(true, 20), makeNai(true, 50), makeNai(false, 90),
+                makeNai(false, 60), makeNai(true, 23), makeNai(false, 68))
+        val bestNetwork = nais[1] // Top score that's satisfying
+        val someRequest = mock(NetworkRequest::class.java)
+        assertEquals(bestNetwork, ranker.getBestNetwork(someRequest, nais, nais[1]))
+    }
+
+    @Test
+    fun testNoMatch() {
+        val nais = listOf(makeNai(false, 20), makeNai(false, 50), makeNai(false, 90))
+        val someRequest = mock(NetworkRequest::class.java)
+        assertNull(ranker.getBestNetwork(someRequest, nais, null))
+    }
+
+    @Test
+    fun testEmpty() {
+        val someRequest = mock(NetworkRequest::class.java)
+        assertNull(ranker.getBestNetwork(someRequest, emptyList(), null))
+    }
+
+    // Make sure the ranker is "stable" (as in stable sort), that is, it always returns the FIRST
+    // network satisfying the request if multiple of them have the same score.
+    @Test
+    fun testStable() {
+        val nais1 = listOf(makeNai(true, 30), makeNai(true, 30), makeNai(true, 30),
+                makeNai(true, 30), makeNai(true, 30), makeNai(true, 30))
+        val someRequest = mock(NetworkRequest::class.java)
+        assertEquals(nais1[0], ranker.getBestNetwork(someRequest, nais1, nais1[0]))
+
+        val nais2 = listOf(makeNai(true, 30), makeNai(true, 50), makeNai(true, 20),
+                makeNai(true, 50), makeNai(true, 50), makeNai(true, 40))
+        assertEquals(nais2[1], ranker.getBestNetwork(someRequest, nais2, nais2[1]))
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
new file mode 100644
index 0000000..8b45755
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -0,0 +1,1042 @@
+/*
+ * 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.connectivity;
+
+import static android.Manifest.permission.CHANGE_NETWORK_STATE;
+import static android.Manifest.permission.CHANGE_WIFI_STATE;
+import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
+import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
+import static android.Manifest.permission.INTERNET;
+import static android.Manifest.permission.NETWORK_STACK;
+import static android.Manifest.permission.UPDATE_DEVICE_STATS;
+import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_OEM;
+import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRODUCT;
+import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_VENDOR;
+import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
+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.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.os.Process.SYSTEM_UID;
+
+import static com.android.server.connectivity.PermissionMonitor.NETWORK;
+import static com.android.server.connectivity.PermissionMonitor.SYSTEM;
+
+import static junit.framework.Assert.fail;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.AdditionalMatchers.aryEq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.net.INetd;
+import android.net.UidRange;
+import android.net.Uri;
+import android.os.Build;
+import android.os.SystemConfigManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.util.SparseIntArray;
+
+import androidx.test.InstrumentationRegistry;
+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.AdditionalAnswers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PermissionMonitorTest {
+    private static final UserHandle MOCK_USER1 = UserHandle.of(0);
+    private static final UserHandle MOCK_USER2 = UserHandle.of(1);
+    private static final int MOCK_UID1 = 10001;
+    private static final int MOCK_UID2 = 10086;
+    private static final int SYSTEM_UID1 = 1000;
+    private static final int SYSTEM_UID2 = 1008;
+    private static final int VPN_UID = 10002;
+    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 SYSTEM_PACKAGE1 = "sysName1";
+    private static final String SYSTEM_PACKAGE2 = "sysName2";
+    private static final String PARTITION_SYSTEM = "system";
+    private static final String PARTITION_OEM = "oem";
+    private static final String PARTITION_PRODUCT = "product";
+    private static final String PARTITION_VENDOR = "vendor";
+    private static final int VERSION_P = Build.VERSION_CODES.P;
+    private static final int VERSION_Q = Build.VERSION_CODES.Q;
+
+    @Mock private Context mContext;
+    @Mock private PackageManager mPackageManager;
+    @Mock private INetd mNetdService;
+    @Mock private UserManager mUserManager;
+    @Mock private PermissionMonitor.Dependencies mDeps;
+    @Mock private SystemConfigManager mSystemConfigManager;
+
+    private PermissionMonitor mPermissionMonitor;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager);
+        when(mUserManager.getUserHandles(eq(true))).thenReturn(
+                Arrays.asList(new UserHandle[] { MOCK_USER1, MOCK_USER2 }));
+        when(mContext.getSystemServiceName(SystemConfigManager.class))
+                .thenReturn(Context.SYSTEM_CONFIG_SERVICE);
+        when(mContext.getSystemService(Context.SYSTEM_CONFIG_SERVICE))
+                .thenReturn(mSystemConfigManager);
+        if (mContext.getSystemService(SystemConfigManager.class) == null) {
+            // Test is using mockito-extended
+            doCallRealMethod().when(mContext).getSystemService(SystemConfigManager.class);
+        }
+        when(mSystemConfigManager.getSystemPermissionUids(anyString())).thenReturn(new int[0]);
+        final Context asUserCtx = mock(Context.class, AdditionalAnswers.delegatesTo(mContext));
+        doReturn(UserHandle.ALL).when(asUserCtx).getUser();
+        when(mContext.createContextAsUser(eq(UserHandle.ALL), anyInt())).thenReturn(asUserCtx);
+        when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>());
+
+        mPermissionMonitor = spy(new PermissionMonitor(mContext, mNetdService, mDeps));
+
+        when(mPackageManager.getInstalledPackages(anyInt())).thenReturn(/* empty app list */ null);
+        mPermissionMonitor.startMonitoring();
+    }
+
+    private boolean hasRestrictedNetworkPermission(String partition, int targetSdkVersion, int uid,
+            String... permissions) {
+        return hasRestrictedNetworkPermission(
+                partition, targetSdkVersion, "" /* packageName */, uid, permissions);
+    }
+
+    private boolean hasRestrictedNetworkPermission(String partition, int targetSdkVersion,
+            String packageName, int uid, String... permissions) {
+        final PackageInfo packageInfo =
+                packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED, permissions, partition);
+        packageInfo.packageName = packageName;
+        packageInfo.applicationInfo.targetSdkVersion = targetSdkVersion;
+        packageInfo.applicationInfo.uid = uid;
+        return mPermissionMonitor.hasRestrictedNetworkPermission(packageInfo);
+    }
+
+    private static PackageInfo systemPackageInfoWithPermissions(String... permissions) {
+        return packageInfoWithPermissions(
+                REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_SYSTEM);
+    }
+
+    private static PackageInfo vendorPackageInfoWithPermissions(String... permissions) {
+        return packageInfoWithPermissions(
+                REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_VENDOR);
+    }
+
+    private static PackageInfo packageInfoWithPermissions(int permissionsFlags,
+            String[] permissions, String partition) {
+        int[] requestedPermissionsFlags = new int[permissions.length];
+        for (int i = 0; i < permissions.length; i++) {
+            requestedPermissionsFlags[i] = permissionsFlags;
+        }
+        final PackageInfo packageInfo = new PackageInfo();
+        packageInfo.requestedPermissions = permissions;
+        packageInfo.applicationInfo = new ApplicationInfo();
+        packageInfo.requestedPermissionsFlags = requestedPermissionsFlags;
+        int privateFlags = 0;
+        switch (partition) {
+            case PARTITION_OEM:
+                privateFlags = PRIVATE_FLAG_OEM;
+                break;
+            case PARTITION_PRODUCT:
+                privateFlags = PRIVATE_FLAG_PRODUCT;
+                break;
+            case PARTITION_VENDOR:
+                privateFlags = PRIVATE_FLAG_VENDOR;
+                break;
+        }
+        packageInfo.applicationInfo.privateFlags = privateFlags;
+        return packageInfo;
+    }
+
+    private static PackageInfo buildPackageInfo(boolean hasSystemPermission, int uid,
+            UserHandle user) {
+        final PackageInfo pkgInfo;
+        if (hasSystemPermission) {
+            pkgInfo = systemPackageInfoWithPermissions(
+                    CHANGE_NETWORK_STATE, NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+        } else {
+            pkgInfo = packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED, new String[] {}, "");
+        }
+        pkgInfo.applicationInfo.uid = user.getUid(UserHandle.getAppId(uid));
+        return pkgInfo;
+    }
+
+    @Test
+    public void testHasPermission() {
+        PackageInfo app = systemPackageInfoWithPermissions();
+        assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE));
+        assertFalse(mPermissionMonitor.hasPermission(app, NETWORK_STACK));
+        assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS));
+        assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_INTERNAL));
+
+        app = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE, NETWORK_STACK);
+        assertTrue(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE));
+        assertTrue(mPermissionMonitor.hasPermission(app, NETWORK_STACK));
+        assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS));
+        assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_INTERNAL));
+
+        app = systemPackageInfoWithPermissions(
+                CONNECTIVITY_USE_RESTRICTED_NETWORKS, CONNECTIVITY_INTERNAL);
+        assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE));
+        assertFalse(mPermissionMonitor.hasPermission(app, NETWORK_STACK));
+        assertTrue(mPermissionMonitor.hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS));
+        assertTrue(mPermissionMonitor.hasPermission(app, CONNECTIVITY_INTERNAL));
+
+        app = packageInfoWithPermissions(REQUESTED_PERMISSION_REQUIRED, new String[] {
+                CONNECTIVITY_USE_RESTRICTED_NETWORKS, CONNECTIVITY_INTERNAL, NETWORK_STACK },
+                PARTITION_SYSTEM);
+        assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE));
+        assertFalse(mPermissionMonitor.hasPermission(app, NETWORK_STACK));
+        assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS));
+        assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_INTERNAL));
+
+        app = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE);
+        app.requestedPermissions = null;
+        assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE));
+
+        app = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE);
+        app.requestedPermissionsFlags = null;
+        assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE));
+    }
+
+    @Test
+    public void testIsVendorApp() {
+        PackageInfo app = systemPackageInfoWithPermissions();
+        assertFalse(mPermissionMonitor.isVendorApp(app.applicationInfo));
+        app = packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED,
+                new String[] {}, PARTITION_OEM);
+        assertTrue(mPermissionMonitor.isVendorApp(app.applicationInfo));
+        app = packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED,
+                new String[] {}, PARTITION_PRODUCT);
+        assertTrue(mPermissionMonitor.isVendorApp(app.applicationInfo));
+        app = vendorPackageInfoWithPermissions();
+        assertTrue(mPermissionMonitor.isVendorApp(app.applicationInfo));
+    }
+
+    @Test
+    public void testHasNetworkPermission() {
+        PackageInfo app = systemPackageInfoWithPermissions();
+        assertFalse(mPermissionMonitor.hasNetworkPermission(app));
+        app = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE);
+        assertTrue(mPermissionMonitor.hasNetworkPermission(app));
+        app = systemPackageInfoWithPermissions(NETWORK_STACK);
+        assertFalse(mPermissionMonitor.hasNetworkPermission(app));
+        app = systemPackageInfoWithPermissions(CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+        assertFalse(mPermissionMonitor.hasNetworkPermission(app));
+        app = systemPackageInfoWithPermissions(CONNECTIVITY_INTERNAL);
+        assertFalse(mPermissionMonitor.hasNetworkPermission(app));
+    }
+
+    @Test
+    public void testHasRestrictedNetworkPermission() {
+        assertFalse(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_P, MOCK_UID1));
+        assertFalse(hasRestrictedNetworkPermission(
+                PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CHANGE_NETWORK_STATE));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_SYSTEM, VERSION_P, MOCK_UID1, NETWORK_STACK));
+        assertFalse(hasRestrictedNetworkPermission(
+                PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CONNECTIVITY_INTERNAL));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CONNECTIVITY_USE_RESTRICTED_NETWORKS));
+        assertFalse(hasRestrictedNetworkPermission(
+                PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CHANGE_WIFI_STATE));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_SYSTEM, VERSION_P, MOCK_UID1, PERMISSION_MAINLINE_NETWORK_STACK));
+
+        assertFalse(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_Q, MOCK_UID1));
+        assertFalse(hasRestrictedNetworkPermission(
+                PARTITION_SYSTEM, VERSION_Q, MOCK_UID1, CONNECTIVITY_INTERNAL));
+    }
+
+    @Test
+    public void testHasRestrictedNetworkPermissionSystemUid() {
+        doReturn(VERSION_P).when(mDeps).getDeviceFirstSdkInt();
+        assertTrue(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_P, SYSTEM_UID));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_SYSTEM, VERSION_P, SYSTEM_UID, CONNECTIVITY_INTERNAL));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_SYSTEM, VERSION_P, SYSTEM_UID, CONNECTIVITY_USE_RESTRICTED_NETWORKS));
+
+        doReturn(VERSION_Q).when(mDeps).getDeviceFirstSdkInt();
+        assertFalse(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID));
+        assertFalse(hasRestrictedNetworkPermission(
+                PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID, CONNECTIVITY_INTERNAL));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID, CONNECTIVITY_USE_RESTRICTED_NETWORKS));
+    }
+
+    @Test
+    public void testHasRestrictedNetworkPermissionVendorApp() {
+        assertTrue(hasRestrictedNetworkPermission(PARTITION_VENDOR, VERSION_P, MOCK_UID1));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_P, MOCK_UID1, CHANGE_NETWORK_STATE));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_P, MOCK_UID1, NETWORK_STACK));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_P, MOCK_UID1, CONNECTIVITY_INTERNAL));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_P, MOCK_UID1, CONNECTIVITY_USE_RESTRICTED_NETWORKS));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_P, MOCK_UID1, CHANGE_WIFI_STATE));
+
+        assertFalse(hasRestrictedNetworkPermission(PARTITION_VENDOR, VERSION_Q, MOCK_UID1));
+        assertFalse(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_Q, MOCK_UID1, CONNECTIVITY_INTERNAL));
+        assertFalse(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_Q, MOCK_UID1, CHANGE_NETWORK_STATE));
+    }
+
+    @Test
+    public void testHasRestrictedNetworkPermissionUidAllowedOnRestrictedNetworks() {
+        mPermissionMonitor.updateUidsAllowedOnRestrictedNetworks(
+                new ArraySet<>(new Integer[] { MOCK_UID1 }));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE1, MOCK_UID1));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE1, MOCK_UID1, CHANGE_NETWORK_STATE));
+        assertTrue(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE1, MOCK_UID1, CONNECTIVITY_INTERNAL));
+
+        assertFalse(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID2));
+        assertFalse(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID2, CHANGE_NETWORK_STATE));
+        assertFalse(hasRestrictedNetworkPermission(
+                PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID2, CONNECTIVITY_INTERNAL));
+
+    }
+
+    private boolean wouldBeCarryoverPackage(String partition, int targetSdkVersion, int uid) {
+        final PackageInfo packageInfo = packageInfoWithPermissions(
+                REQUESTED_PERMISSION_GRANTED, new String[] {}, partition);
+        packageInfo.applicationInfo.targetSdkVersion = targetSdkVersion;
+        packageInfo.applicationInfo.uid = uid;
+        return mPermissionMonitor.isCarryoverPackage(packageInfo.applicationInfo);
+    }
+
+    @Test
+    public void testIsCarryoverPackage() {
+        doReturn(VERSION_P).when(mDeps).getDeviceFirstSdkInt();
+        assertTrue(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_P, SYSTEM_UID));
+        assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_P, SYSTEM_UID));
+        assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_P, MOCK_UID1));
+        assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_P, MOCK_UID1));
+        assertTrue(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID));
+        assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_Q, SYSTEM_UID));
+        assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_Q, MOCK_UID1));
+        assertFalse(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_Q, MOCK_UID1));
+
+        doReturn(VERSION_Q).when(mDeps).getDeviceFirstSdkInt();
+        assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_P, SYSTEM_UID));
+        assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_P, SYSTEM_UID));
+        assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_P, MOCK_UID1));
+        assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_P, MOCK_UID1));
+        assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID));
+        assertFalse(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_Q, SYSTEM_UID));
+        assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_Q, MOCK_UID1));
+        assertFalse(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_Q, MOCK_UID1));
+
+        assertFalse(wouldBeCarryoverPackage(PARTITION_OEM, VERSION_Q, SYSTEM_UID));
+        assertFalse(wouldBeCarryoverPackage(PARTITION_PRODUCT, VERSION_Q, SYSTEM_UID));
+        assertFalse(wouldBeCarryoverPackage(PARTITION_OEM, VERSION_Q, MOCK_UID1));
+        assertFalse(wouldBeCarryoverPackage(PARTITION_PRODUCT, VERSION_Q, MOCK_UID1));
+    }
+
+    private boolean wouldBeUidAllowedOnRestrictedNetworks(int uid) {
+        final ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.uid = uid;
+        return mPermissionMonitor.isUidAllowedOnRestrictedNetworks(applicationInfo);
+    }
+
+    @Test
+    public void testIsAppAllowedOnRestrictedNetworks() {
+        mPermissionMonitor.updateUidsAllowedOnRestrictedNetworks(new ArraySet<>());
+        assertFalse(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID1));
+        assertFalse(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID2));
+
+        mPermissionMonitor.updateUidsAllowedOnRestrictedNetworks(
+                new ArraySet<>(new Integer[] { MOCK_UID1 }));
+        assertTrue(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID1));
+        assertFalse(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID2));
+
+        mPermissionMonitor.updateUidsAllowedOnRestrictedNetworks(
+                new ArraySet<>(new Integer[] { MOCK_UID2 }));
+        assertFalse(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID1));
+        assertTrue(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID2));
+
+        mPermissionMonitor.updateUidsAllowedOnRestrictedNetworks(
+                new ArraySet<>(new Integer[] { 123 }));
+        assertFalse(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID1));
+        assertFalse(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID2));
+    }
+
+    private void assertBackgroundPermission(boolean hasPermission, String name, int uid,
+            String... permissions) throws Exception {
+        when(mPackageManager.getPackageInfo(eq(name), anyInt()))
+                .thenReturn(packageInfoWithPermissions(
+                        REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_SYSTEM));
+        mPermissionMonitor.onPackageAdded(name, uid);
+        assertEquals(hasPermission, mPermissionMonitor.hasUseBackgroundNetworksPermission(uid));
+    }
+
+    @Test
+    public void testHasUseBackgroundNetworksPermission() throws Exception {
+        assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(SYSTEM_UID));
+        assertBackgroundPermission(false, SYSTEM_PACKAGE1, SYSTEM_UID);
+        assertBackgroundPermission(false, SYSTEM_PACKAGE1, SYSTEM_UID, CONNECTIVITY_INTERNAL);
+        assertBackgroundPermission(true, SYSTEM_PACKAGE1, SYSTEM_UID, CHANGE_NETWORK_STATE);
+        assertBackgroundPermission(true, SYSTEM_PACKAGE1, SYSTEM_UID, NETWORK_STACK);
+
+        assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(MOCK_UID1));
+        assertBackgroundPermission(false, MOCK_PACKAGE1, MOCK_UID1);
+        assertBackgroundPermission(true, MOCK_PACKAGE1, MOCK_UID1,
+                CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+
+        assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(MOCK_UID2));
+        assertBackgroundPermission(false, MOCK_PACKAGE2, MOCK_UID2);
+        assertBackgroundPermission(false, MOCK_PACKAGE2, MOCK_UID2,
+                CONNECTIVITY_INTERNAL);
+        assertBackgroundPermission(true, MOCK_PACKAGE2, MOCK_UID2, NETWORK_STACK);
+    }
+
+    private class NetdMonitor {
+        private final HashMap<Integer, Boolean> mApps = new HashMap<>();
+
+        NetdMonitor(INetd mockNetd) throws Exception {
+            // Add hook to verify and track result of setPermission.
+            doAnswer((InvocationOnMock invocation) -> {
+                final Object[] args = invocation.getArguments();
+                final Boolean isSystem = args[0].equals(INetd.PERMISSION_SYSTEM);
+                for (final int uid : (int[]) args[1]) {
+                    // TODO: Currently, permission monitor will send duplicate commands for each uid
+                    // corresponding to each user. Need to fix that and uncomment below test.
+                    // if (mApps.containsKey(uid) && mApps.get(uid) == isSystem) {
+                    //     fail("uid " + uid + " is already set to " + isSystem);
+                    // }
+                    mApps.put(uid, isSystem);
+                }
+                return null;
+            }).when(mockNetd).networkSetPermissionForUser(anyInt(), any(int[].class));
+
+            // Add hook to verify and track result of clearPermission.
+            doAnswer((InvocationOnMock invocation) -> {
+                final Object[] args = invocation.getArguments();
+                for (final int uid : (int[]) args[0]) {
+                    // TODO: Currently, permission monitor will send duplicate commands for each uid
+                    // corresponding to each user. Need to fix that and uncomment below test.
+                    // if (!mApps.containsKey(uid)) {
+                    //     fail("uid " + uid + " does not exist.");
+                    // }
+                    mApps.remove(uid);
+                }
+                return null;
+            }).when(mockNetd).networkClearPermissionForUser(any(int[].class));
+        }
+
+        public void expectPermission(Boolean permission, UserHandle[] users, int[] apps) {
+            for (final UserHandle user : users) {
+                for (final int app : apps) {
+                    final int uid = user.getUid(app);
+                    if (!mApps.containsKey(uid)) {
+                        fail("uid " + uid + " does not exist.");
+                    }
+                    if (mApps.get(uid) != permission) {
+                        fail("uid " + uid + " has wrong permission: " +  permission);
+                    }
+                }
+            }
+        }
+
+        public void expectNoPermission(UserHandle[] users, int[] apps) {
+            for (final UserHandle user : users) {
+                for (final int app : apps) {
+                    final int uid = user.getUid(app);
+                    if (mApps.containsKey(uid)) {
+                        fail("uid " + uid + " has listed permissions, expected none.");
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testUserAndPackageAddRemove() throws Exception {
+        final NetdMonitor netdMonitor = new NetdMonitor(mNetdService);
+
+        // MOCK_UID1: MOCK_PACKAGE1 only has network permission.
+        // SYSTEM_UID: SYSTEM_PACKAGE1 has system permission.
+        // SYSTEM_UID: SYSTEM_PACKAGE2 only has network permission.
+        doReturn(SYSTEM).when(mPermissionMonitor).highestPermissionForUid(eq(SYSTEM), anyString());
+        doReturn(SYSTEM).when(mPermissionMonitor).highestPermissionForUid(any(),
+                eq(SYSTEM_PACKAGE1));
+        doReturn(NETWORK).when(mPermissionMonitor).highestPermissionForUid(any(),
+                eq(SYSTEM_PACKAGE2));
+        doReturn(NETWORK).when(mPermissionMonitor).highestPermissionForUid(any(),
+                eq(MOCK_PACKAGE1));
+
+        // Add SYSTEM_PACKAGE2, expect only have network permission.
+        mPermissionMonitor.onUserAdded(MOCK_USER1);
+        addPackageForUsers(new UserHandle[]{MOCK_USER1}, SYSTEM_PACKAGE2, SYSTEM_UID);
+        netdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{SYSTEM_UID});
+
+        // Add SYSTEM_PACKAGE1, expect permission escalate.
+        addPackageForUsers(new UserHandle[]{MOCK_USER1}, SYSTEM_PACKAGE1, SYSTEM_UID);
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{SYSTEM_UID});
+
+        mPermissionMonitor.onUserAdded(MOCK_USER2);
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2},
+                new int[]{SYSTEM_UID});
+
+        addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_UID1);
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2},
+                new int[]{SYSTEM_UID});
+        netdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1, MOCK_USER2},
+                new int[]{MOCK_UID1});
+
+        // Remove MOCK_UID1, expect no permission left for all user.
+        mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1);
+        removePackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_UID1);
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2},
+                new int[]{MOCK_UID1});
+
+        // Remove SYSTEM_PACKAGE1, expect permission downgrade.
+        when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(new String[]{SYSTEM_PACKAGE2});
+        removePackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2},
+                SYSTEM_PACKAGE1, SYSTEM_UID);
+        netdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1, MOCK_USER2},
+                new int[]{SYSTEM_UID});
+
+        mPermissionMonitor.onUserRemoved(MOCK_USER1);
+        netdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER2}, new int[]{SYSTEM_UID});
+
+        // Remove all packages, expect no permission left.
+        when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(new String[]{});
+        removePackageForUsers(new UserHandle[]{MOCK_USER2}, SYSTEM_PACKAGE2, SYSTEM_UID);
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2},
+                new int[]{SYSTEM_UID, MOCK_UID1});
+
+        // Remove last user, expect no redundant clearPermission is invoked.
+        mPermissionMonitor.onUserRemoved(MOCK_USER2);
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2},
+                new int[]{SYSTEM_UID, MOCK_UID1});
+    }
+
+    @Test
+    public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
+        when(mPackageManager.getInstalledPackages(eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn(
+                Arrays.asList(new PackageInfo[] {
+                        buildPackageInfo(true /* hasSystemPermission */, SYSTEM_UID1, MOCK_USER1),
+                        buildPackageInfo(false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1),
+                        buildPackageInfo(false /* hasSystemPermission */, MOCK_UID2, MOCK_USER1),
+                        buildPackageInfo(false /* hasSystemPermission */, VPN_UID, MOCK_USER1)
+                }));
+        when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE1),
+                eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn(
+                buildPackageInfo(false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1));
+        mPermissionMonitor.startMonitoring();
+        // Every app on user 0 except MOCK_UID2 are under VPN.
+        final Set<UidRange> vpnRange1 = new HashSet<>(Arrays.asList(new UidRange[] {
+                new UidRange(0, MOCK_UID2 - 1),
+                new UidRange(MOCK_UID2 + 1, UserHandle.PER_USER_RANGE - 1)}));
+        final Set<UidRange> vpnRange2 = Collections.singleton(new UidRange(MOCK_UID2, MOCK_UID2));
+
+        // When VPN is connected, expect a rule to be set up for user app MOCK_UID1
+        mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange1, VPN_UID);
+        verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"),
+                aryEq(new int[] {MOCK_UID1}));
+
+        reset(mNetdService);
+
+        // When MOCK_UID1 package is uninstalled and reinstalled, expect Netd to be updated
+        mPermissionMonitor.onPackageRemoved(
+                MOCK_PACKAGE1, MOCK_USER1.getUid(MOCK_UID1));
+        verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID1}));
+        mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_USER1.getUid(MOCK_UID1));
+        verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"),
+                aryEq(new int[] {MOCK_UID1}));
+
+        reset(mNetdService);
+
+        // 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);
+        verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID1}));
+        mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange2, VPN_UID);
+        verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"),
+                aryEq(new int[] {MOCK_UID2}));
+
+        reset(mNetdService);
+
+        // When VPN is disconnected, expect rules to be torn down
+        mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange2, VPN_UID);
+        verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID2}));
+        assertNull(mPermissionMonitor.getVpnUidRanges("tun0"));
+    }
+
+    @Test
+    public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
+        when(mPackageManager.getInstalledPackages(eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn(
+                Arrays.asList(new PackageInfo[] {
+                        buildPackageInfo(true /* hasSystemPermission */, SYSTEM_UID1, MOCK_USER1),
+                        buildPackageInfo(false /* hasSystemPermission */, VPN_UID, MOCK_USER1)
+                }));
+        when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE1),
+                eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn(
+                buildPackageInfo(false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1));
+
+        mPermissionMonitor.startMonitoring();
+        final Set<UidRange> vpnRange = Collections.singleton(UidRange.createForUser(MOCK_USER1));
+        mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange, VPN_UID);
+
+        // Newly-installed package should have uid rules added
+        mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_USER1.getUid(MOCK_UID1));
+        verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"),
+                aryEq(new int[] {MOCK_UID1}));
+
+        // Removed package should have its uid rules removed
+        mPermissionMonitor.onPackageRemoved(
+                MOCK_PACKAGE1, MOCK_USER1.getUid(MOCK_UID1));
+        verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID1}));
+    }
+
+
+    // 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
+    // called multiple times with the uid corresponding to each user.
+    private void addPackageForUsers(UserHandle[] users, String packageName, int uid) {
+        for (final UserHandle user : users) {
+            mPermissionMonitor.onPackageAdded(packageName, user.getUid(uid));
+        }
+    }
+
+    private void removePackageForUsers(UserHandle[] users, String packageName, int uid) {
+        for (final UserHandle user : users) {
+            mPermissionMonitor.onPackageRemoved(packageName, user.getUid(uid));
+        }
+    }
+
+    private class NetdServiceMonitor {
+        private final HashMap<Integer, Integer> mPermissions = new HashMap<>();
+
+        NetdServiceMonitor(INetd mockNetdService) throws Exception {
+            // Add hook to verify and track result of setPermission.
+            doAnswer((InvocationOnMock invocation) -> {
+                final Object[] args = invocation.getArguments();
+                final int permission = (int) args[0];
+                for (final int uid : (int[]) args[1]) {
+                    mPermissions.put(uid, permission);
+                }
+                return null;
+            }).when(mockNetdService).trafficSetNetPermForUids(anyInt(), any(int[].class));
+        }
+
+        public void expectPermission(int permission, int[] apps) {
+            for (final int app : apps) {
+                if (!mPermissions.containsKey(app)) {
+                    fail("uid " + app + " does not exist.");
+                }
+                if (mPermissions.get(app) != permission) {
+                    fail("uid " + app + " has wrong permission: " + mPermissions.get(app));
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testPackagePermissionUpdate() throws Exception {
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
+        // MOCK_UID1: MOCK_PACKAGE1 only has internet permission.
+        // MOCK_UID2: MOCK_PACKAGE2 does not have any permission.
+        // SYSTEM_UID1: SYSTEM_PACKAGE1 has internet permission and update device stats permission.
+        // SYSTEM_UID2: SYSTEM_PACKAGE2 has only update device stats permission.
+
+        SparseIntArray netdPermissionsAppIds = new SparseIntArray();
+        netdPermissionsAppIds.put(MOCK_UID1, INetd.PERMISSION_INTERNET);
+        netdPermissionsAppIds.put(MOCK_UID2, INetd.PERMISSION_NONE);
+        netdPermissionsAppIds.put(SYSTEM_UID1, INetd.PERMISSION_INTERNET
+                | INetd.PERMISSION_UPDATE_DEVICE_STATS);
+        netdPermissionsAppIds.put(SYSTEM_UID2, INetd.PERMISSION_UPDATE_DEVICE_STATS);
+
+        // Send the permission information to netd, expect permission updated.
+        mPermissionMonitor.sendPackagePermissionsToNetd(netdPermissionsAppIds);
+
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET,
+                new int[]{MOCK_UID1});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{MOCK_UID2});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+                | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{SYSTEM_UID1});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_UPDATE_DEVICE_STATS,
+                new int[]{SYSTEM_UID2});
+
+        // Update permission of MOCK_UID1, expect new permission show up.
+        mPermissionMonitor.sendPackagePermissionsForUid(MOCK_UID1,
+                INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS);
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+                | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
+
+        // Change permissions of SYSTEM_UID2, expect new permission show up and old permission
+        // revoked.
+        mPermissionMonitor.sendPackagePermissionsForUid(SYSTEM_UID2,
+                INetd.PERMISSION_INTERNET);
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{SYSTEM_UID2});
+
+        // Revoke permission from SYSTEM_UID1, expect no permission stored.
+        mPermissionMonitor.sendPackagePermissionsForUid(SYSTEM_UID1, INetd.PERMISSION_NONE);
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{SYSTEM_UID1});
+    }
+
+    private PackageInfo setPackagePermissions(String packageName, int uid, String[] permissions)
+            throws Exception {
+        PackageInfo packageInfo = packageInfoWithPermissions(
+                REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_SYSTEM);
+        when(mPackageManager.getPackageInfo(eq(packageName), anyInt())).thenReturn(packageInfo);
+        when(mPackageManager.getPackagesForUid(eq(uid))).thenReturn(new String[]{packageName});
+        return packageInfo;
+    }
+
+    private PackageInfo addPackage(String packageName, int uid, String[] permissions)
+            throws Exception {
+        PackageInfo packageInfo = setPackagePermissions(packageName, uid, permissions);
+        mPermissionMonitor.onPackageAdded(packageName, uid);
+        return packageInfo;
+    }
+
+    @Test
+    public void testPackageInstall() throws Exception {
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
+
+        addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+                | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
+
+        addPackage(MOCK_PACKAGE2, MOCK_UID2, new String[] {INTERNET});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID2});
+    }
+
+    @Test
+    public void testPackageInstallSharedUid() throws Exception {
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
+
+        PackageInfo packageInfo1 = addPackage(MOCK_PACKAGE1, MOCK_UID1,
+                new String[] {INTERNET, UPDATE_DEVICE_STATS});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+                | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
+
+        // Install another package with the same uid and no permissions should not cause the UID to
+        // lose permissions.
+        PackageInfo packageInfo2 = systemPackageInfoWithPermissions();
+        when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2);
+        when(mPackageManager.getPackagesForUid(MOCK_UID1))
+              .thenReturn(new String[]{MOCK_PACKAGE1, MOCK_PACKAGE2});
+        mPermissionMonitor.onPackageAdded(MOCK_PACKAGE2, MOCK_UID1);
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+                | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
+    }
+
+    @Test
+    public void testPackageUninstallBasic() throws Exception {
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
+
+        addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+                | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
+
+        when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{});
+        mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1);
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[]{MOCK_UID1});
+    }
+
+    @Test
+    public void testPackageRemoveThenAdd() throws Exception {
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
+
+        addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+                | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
+
+        when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{});
+        mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1);
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[]{MOCK_UID1});
+
+        addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1});
+    }
+
+    @Test
+    public void testPackageUpdate() throws Exception {
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
+
+        addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{MOCK_UID1});
+
+        addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1});
+    }
+
+    @Test
+    public void testPackageUninstallWithMultiplePackages() throws Exception {
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
+
+        addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS});
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+                | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1});
+
+        // Mock another package with the same uid but different permissions.
+        PackageInfo packageInfo2 = systemPackageInfoWithPermissions(INTERNET);
+        when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2);
+        when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{
+                MOCK_PACKAGE2});
+
+        mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1);
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1});
+    }
+
+    @Test
+    public void testRealSystemPermission() throws Exception {
+        // Use the real context as this test must ensure the *real* system package holds the
+        // necessary permission.
+        final Context realContext = InstrumentationRegistry.getContext();
+        final PermissionMonitor monitor = new PermissionMonitor(realContext, mNetdService);
+        final PackageManager manager = realContext.getPackageManager();
+        final PackageInfo systemInfo = manager.getPackageInfo(REAL_SYSTEM_PACKAGE_NAME,
+                GET_PERMISSIONS | MATCH_ANY_USER);
+        assertTrue(monitor.hasPermission(systemInfo, CONNECTIVITY_USE_RESTRICTED_NETWORKS));
+    }
+
+    @Test
+    public void testUpdateUidPermissionsFromSystemConfig() throws Exception {
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
+        when(mPackageManager.getInstalledPackages(anyInt())).thenReturn(new ArrayList<>());
+        when(mSystemConfigManager.getSystemPermissionUids(eq(INTERNET)))
+                .thenReturn(new int[]{ MOCK_UID1, MOCK_UID2 });
+        when(mSystemConfigManager.getSystemPermissionUids(eq(UPDATE_DEVICE_STATS)))
+                .thenReturn(new int[]{ MOCK_UID2 });
+
+        mPermissionMonitor.startMonitoring();
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{ MOCK_UID1 });
+        netdServiceMonitor.expectPermission(
+                INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS,
+                new int[]{ MOCK_UID2 });
+    }
+
+    @Test
+    public void testIntentReceiver() throws Exception {
+        final NetdServiceMonitor netdServiceMonitor = new NetdServiceMonitor(mNetdService);
+        final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        verify(mContext, times(1)).registerReceiver(receiverCaptor.capture(), any(), any(), any());
+        final BroadcastReceiver receiver = receiverCaptor.getValue();
+
+        // Verify receiving PACKAGE_ADDED intent.
+        final Intent addedIntent = new Intent(Intent.ACTION_PACKAGE_ADDED,
+                Uri.fromParts("package", MOCK_PACKAGE1, null /* fragment */));
+        addedIntent.putExtra(Intent.EXTRA_UID, MOCK_UID1);
+        setPackagePermissions(MOCK_PACKAGE1, MOCK_UID1,
+                new String[] { INTERNET, UPDATE_DEVICE_STATS });
+        receiver.onReceive(mContext, addedIntent);
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET
+                | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[] { MOCK_UID1 });
+
+        // Verify receiving PACKAGE_REMOVED intent.
+        when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(null);
+        final Intent removedIntent = new Intent(Intent.ACTION_PACKAGE_REMOVED,
+                Uri.fromParts("package", MOCK_PACKAGE1, null /* fragment */));
+        removedIntent.putExtra(Intent.EXTRA_UID, MOCK_UID1);
+        receiver.onReceive(mContext, removedIntent);
+        netdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[] { MOCK_UID1 });
+    }
+
+    private ContentObserver expectRegisterContentObserver(Uri expectedUri) {
+        final ArgumentCaptor<ContentObserver> captor =
+                ArgumentCaptor.forClass(ContentObserver.class);
+        verify(mDeps).registerContentObserver(any(),
+                argThat(uri -> uri.equals(expectedUri)), anyBoolean(), captor.capture());
+        return captor.getValue();
+    }
+
+    private void buildAndMockPackageInfoWithPermissions(String packageName, int uid,
+            String... permissions) throws Exception {
+        final PackageInfo packageInfo = setPackagePermissions(packageName, uid, permissions);
+        packageInfo.packageName = packageName;
+        packageInfo.applicationInfo.uid = uid;
+    }
+
+    @Test
+    public void testUidsAllowedOnRestrictedNetworksChanged() throws Exception {
+        final NetdMonitor netdMonitor = new NetdMonitor(mNetdService);
+        final ContentObserver contentObserver = expectRegisterContentObserver(
+                Settings.Global.getUriFor(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS));
+
+        mPermissionMonitor.onUserAdded(MOCK_USER1);
+        // Prepare PackageInfo for MOCK_PACKAGE1 and MOCK_PACKAGE2
+        buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID1);
+        buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE2, MOCK_UID2);
+
+        // MOCK_UID1 is listed in setting that allow to use restricted networks, MOCK_UID1
+        // should have SYSTEM permission.
+        when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(
+                new ArraySet<>(new Integer[] { MOCK_UID1 }));
+        contentObserver.onChange(true /* selfChange */);
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2});
+
+        // MOCK_UID2 is listed in setting that allow to use restricted networks, MOCK_UID2
+        // should have SYSTEM permission but MOCK_UID1 should revoke permission.
+        when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(
+                new ArraySet<>(new Integer[] { MOCK_UID2 }));
+        contentObserver.onChange(true /* selfChange */);
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2});
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+
+        // No uid lists in setting, should revoke permission from all uids.
+        when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>());
+        contentObserver.onChange(true /* selfChange */);
+        netdMonitor.expectNoPermission(
+                new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1, MOCK_UID2});
+    }
+
+    @Test
+    public void testUidsAllowedOnRestrictedNetworksChangedWithSharedUid() throws Exception {
+        final NetdMonitor netdMonitor = new NetdMonitor(mNetdService);
+        final ContentObserver contentObserver = expectRegisterContentObserver(
+                Settings.Global.getUriFor(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS));
+
+        mPermissionMonitor.onUserAdded(MOCK_USER1);
+        buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID1, CHANGE_NETWORK_STATE);
+        buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE2, MOCK_UID1);
+        when(mPackageManager.getPackagesForUid(MOCK_UID1))
+                .thenReturn(new String[]{MOCK_PACKAGE1, MOCK_PACKAGE2});
+
+        // MOCK_PACKAGE1 have CHANGE_NETWORK_STATE, MOCK_UID1 should have NETWORK permission.
+        addPackageForUsers(new UserHandle[]{MOCK_USER1}, MOCK_PACKAGE1, MOCK_UID1);
+        netdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+
+        // MOCK_UID1 is listed in setting that allow to use restricted networks, MOCK_UID1
+        // should upgrade to SYSTEM permission.
+        when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(
+                new ArraySet<>(new Integer[] { MOCK_UID1 }));
+        contentObserver.onChange(true /* selfChange */);
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+
+        // No app lists in setting, MOCK_UID1 should downgrade to NETWORK permission.
+        when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>());
+        contentObserver.onChange(true /* selfChange */);
+        netdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+
+        // MOCK_PACKAGE1 removed, should revoke permission from MOCK_UID1.
+        when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{MOCK_PACKAGE2});
+        removePackageForUsers(new UserHandle[]{MOCK_USER1}, MOCK_PACKAGE1, MOCK_UID1);
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+    }
+
+    @Test
+    public void testUidsAllowedOnRestrictedNetworksChangedWithMultipleUsers() throws Exception {
+        final NetdMonitor netdMonitor = new NetdMonitor(mNetdService);
+        final ContentObserver contentObserver = expectRegisterContentObserver(
+                Settings.Global.getUriFor(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS));
+
+        // One user MOCK_USER1
+        mPermissionMonitor.onUserAdded(MOCK_USER1);
+        // Prepare PackageInfo for MOCK_PACKAGE1 and MOCK_PACKAGE2.
+        buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID1);
+        buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE2, MOCK_UID2);
+
+        // MOCK_UID1 is listed in setting that allow to use restricted networks, MOCK_UID1
+        // in MOCK_USER1 should have SYSTEM permission and MOCK_UID2 has no permissions.
+        when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(
+                new ArraySet<>(new Integer[] { MOCK_UID1 }));
+        contentObserver.onChange(true /* selfChange */);
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+        netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2});
+
+        // Add user MOCK_USER2.
+        mPermissionMonitor.onUserAdded(MOCK_USER2);
+        // MOCK_UID1 in both users should all have SYSTEM permission and MOCK_UID2 has no
+        // permissions in either user.
+        netdMonitor.expectPermission(
+                SYSTEM, new UserHandle[] { MOCK_USER1, MOCK_USER2 }, new int[]{MOCK_UID1});
+        netdMonitor.expectNoPermission(
+                new UserHandle[] { MOCK_USER1, MOCK_USER2 }, new int[]{MOCK_UID2});
+
+        // MOCK_UID2 is listed in setting that allow to use restricted networks, MOCK_UID2
+        // in both users should have SYSTEM permission and MOCK_UID1 has no permissions.
+        when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(
+                new ArraySet<>(new Integer[] { MOCK_UID2 }));
+        contentObserver.onChange(true /* selfChange */);
+        netdMonitor.expectPermission(
+                SYSTEM, new UserHandle[] { MOCK_USER1, MOCK_USER2 }, new int[]{MOCK_UID2});
+        netdMonitor.expectNoPermission(
+                new UserHandle[] { MOCK_USER1, MOCK_USER2 }, new int[]{MOCK_UID1});
+
+        // Remove user MOCK_USER1
+        mPermissionMonitor.onUserRemoved(MOCK_USER1);
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[] {MOCK_USER2}, new int[]{MOCK_UID2});
+        netdMonitor.expectNoPermission(new UserHandle[] {MOCK_USER2}, new int[]{MOCK_UID1});
+
+        // No uid lists in setting, should revoke permission from all uids.
+        when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>());
+        contentObserver.onChange(true /* selfChange */);
+        netdMonitor.expectNoPermission(
+                new UserHandle[]{MOCK_USER2}, new int[]{ MOCK_UID1, MOCK_UID2 });
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
new file mode 100644
index 0000000..6ff47ae
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -0,0 +1,1286 @@
+/*
+ * 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.connectivity;
+
+import static android.content.pm.UserInfo.FLAG_ADMIN;
+import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
+import static android.content.pm.UserInfo.FLAG_PRIMARY;
+import static android.content.pm.UserInfo.FLAG_RESTRICTED;
+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.os.UserHandle.PER_USER_RANGE;
+
+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.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.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.AppOpsManager;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.Ikev2VpnProfile;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.net.IpSecManager;
+import android.net.IpSecTunnelInterfaceResponse;
+import android.net.LinkProperties;
+import android.net.LocalSocket;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo.DetailedState;
+import android.net.RouteInfo;
+import android.net.UidRangeParcel;
+import android.net.VpnManager;
+import android.net.VpnService;
+import android.net.VpnTransportInfo;
+import android.net.ipsec.ike.IkeSessionCallback;
+import android.net.ipsec.ike.exceptions.IkeProtocolException;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.os.INetworkManagementService;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.test.TestLooper;
+import android.provider.Settings;
+import android.security.Credentials;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Range;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.internal.net.LegacyVpnInfo;
+import com.android.internal.net.VpnConfig;
+import com.android.internal.net.VpnProfile;
+import com.android.server.IpSecService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.AdditionalAnswers;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+
+/**
+ * Tests for {@link Vpn}.
+ *
+ * Build, install and run with:
+ *  runtest frameworks-net -c com.android.server.connectivity.VpnTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VpnTest {
+    private static final String TAG = "VpnTest";
+
+    // Mock users
+    static final UserInfo primaryUser = new UserInfo(27, "Primary", FLAG_ADMIN | FLAG_PRIMARY);
+    static final UserInfo secondaryUser = new UserInfo(15, "Secondary", FLAG_ADMIN);
+    static final UserInfo restrictedProfileA = new UserInfo(40, "RestrictedA", FLAG_RESTRICTED);
+    static final UserInfo restrictedProfileB = new UserInfo(42, "RestrictedB", FLAG_RESTRICTED);
+    static final UserInfo managedProfileA = new UserInfo(45, "ManagedA", FLAG_MANAGED_PROFILE);
+    static {
+        restrictedProfileA.restrictedProfileParentId = primaryUser.id;
+        restrictedProfileB.restrictedProfileParentId = secondaryUser.id;
+        managedProfileA.profileGroupId = primaryUser.id;
+    }
+
+    static final Network EGRESS_NETWORK = new Network(101);
+    static final String EGRESS_IFACE = "wlan0";
+    static final String TEST_VPN_PKG = "com.testvpn.vpn";
+    private static final String TEST_VPN_SERVER = "1.2.3.4";
+    private static final String TEST_VPN_IDENTITY = "identity";
+    private static final byte[] TEST_VPN_PSK = "psk".getBytes();
+
+    private static final Network TEST_NETWORK = new Network(Integer.MAX_VALUE);
+    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;
+
+    /**
+     * 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};
+
+    // Mock packages
+    static final Map<String, Integer> mPackages = new ArrayMap<>();
+    static {
+        for (int i = 0; i < PKGS.length; i++) {
+            mPackages.put(PKGS[i], PKG_UIDS[i]);
+        }
+    }
+    private static final Range<Integer> PRI_USER_RANGE = uidRangeForUser(primaryUser.id);
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext;
+    @Mock private UserManager mUserManager;
+    @Mock private PackageManager mPackageManager;
+    @Mock private INetworkManagementService mNetService;
+    @Mock private INetd mNetd;
+    @Mock private AppOpsManager mAppOps;
+    @Mock private NotificationManager mNotificationManager;
+    @Mock private Vpn.SystemServices mSystemServices;
+    @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator;
+    @Mock private ConnectivityManager mConnectivityManager;
+    @Mock private IpSecService mIpSecService;
+    @Mock private VpnProfileStore mVpnProfileStore;
+    private final VpnProfile mVpnProfile;
+
+    private IpSecManager mIpSecManager;
+
+    public VpnTest() throws Exception {
+        // Build an actual VPN profile that is capable of being converted to and from an
+        // Ikev2VpnProfile
+        final Ikev2VpnProfile.Builder builder =
+                new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY);
+        builder.setAuthPsk(TEST_VPN_PSK);
+        mVpnProfile = builder.build().toVpnProfile();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mIpSecManager = new IpSecManager(mContext, mIpSecService);
+
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        setMockedPackages(mPackages);
+
+        when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG);
+        when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG);
+        mockService(UserManager.class, Context.USER_SERVICE, mUserManager);
+        mockService(AppOpsManager.class, Context.APP_OPS_SERVICE, mAppOps);
+        mockService(NotificationManager.class, Context.NOTIFICATION_SERVICE, mNotificationManager);
+        mockService(ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mConnectivityManager);
+        mockService(IpSecManager.class, Context.IPSEC_SERVICE, mIpSecManager);
+        when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent))
+                .thenReturn(Resources.getSystem().getString(
+                        R.string.config_customVpnAlwaysOnDisconnectedDialogComponent));
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS))
+                .thenReturn(true);
+
+        // Used by {@link Notification.Builder}
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT;
+        when(mContext.getApplicationInfo()).thenReturn(applicationInfo);
+        when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenReturn(applicationInfo);
+
+        doNothing().when(mNetService).registerObserver(any());
+
+        // Deny all appops by default.
+        when(mAppOps.noteOpNoThrow(anyString(), anyInt(), anyString(), any(), any()))
+                .thenReturn(AppOpsManager.MODE_IGNORED);
+
+        // Setup IpSecService
+        final IpSecTunnelInterfaceResponse tunnelResp =
+                new IpSecTunnelInterfaceResponse(
+                        IpSecManager.Status.OK, TEST_TUNNEL_RESOURCE_ID, TEST_IFACE_NAME);
+        when(mIpSecService.createTunnelInterface(any(), any(), any(), any(), any()))
+                .thenReturn(tunnelResp);
+    }
+
+    private <T> void mockService(Class<T> clazz, String name, T service) {
+        doReturn(service).when(mContext).getSystemService(name);
+        doReturn(name).when(mContext).getSystemServiceName(clazz);
+        if (mContext.getSystemService(clazz).getClass().equals(Object.class)) {
+            // Test is using mockito-extended (mContext uses Answers.RETURNS_DEEP_STUBS and returned
+            // a mock object on a final method)
+            doCallRealMethod().when(mContext).getSystemService(clazz);
+        }
+    }
+
+    private Set<Range<Integer>> rangeSet(Range<Integer> ... ranges) {
+        final Set<Range<Integer>> range = new ArraySet<>();
+        for (Range<Integer> r : ranges) range.add(r);
+
+        return range;
+    }
+
+    private static Range<Integer> uidRangeForUser(int userId) {
+        return new Range<Integer>(userId * PER_USER_RANGE, (userId + 1) * PER_USER_RANGE - 1);
+    }
+
+    private Range<Integer> uidRange(int start, int stop) {
+        return new Range<Integer>(start, stop);
+    }
+
+    @Test
+    public void testRestrictedProfilesAreAddedToVpn() {
+        setMockedUsers(primaryUser, secondaryUser, restrictedProfileA, restrictedProfileB);
+
+        final Vpn vpn = createVpn(primaryUser.id);
+
+        // Assume the user can have restricted profiles.
+        doReturn(true).when(mUserManager).canHaveRestrictedProfile();
+        final Set<Range<Integer>> ranges =
+                vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, null, null);
+
+        assertEquals(rangeSet(PRI_USER_RANGE, uidRangeForUser(restrictedProfileA.id)), ranges);
+    }
+
+    @Test
+    public void testManagedProfilesAreNotAddedToVpn() {
+        setMockedUsers(primaryUser, managedProfileA);
+
+        final Vpn vpn = createVpn(primaryUser.id);
+        final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
+                null, null);
+
+        assertEquals(rangeSet(PRI_USER_RANGE), ranges);
+    }
+
+    @Test
+    public void testAddUserToVpnOnlyAddsOneUser() {
+        setMockedUsers(primaryUser, restrictedProfileA, managedProfileA);
+
+        final Vpn vpn = createVpn(primaryUser.id);
+        final Set<Range<Integer>> ranges = new ArraySet<>();
+        vpn.addUserToRanges(ranges, primaryUser.id, null, null);
+
+        assertEquals(rangeSet(PRI_USER_RANGE), ranges);
+    }
+
+    @Test
+    public void testUidAllowAndDenylist() throws Exception {
+        final Vpn vpn = createVpn(primaryUser.id);
+        final Range<Integer> user = PRI_USER_RANGE;
+        final int userStart = user.getLower();
+        final int userStop = user.getUpper();
+        final String[] packages = {PKGS[0], PKGS[1], PKGS[2]};
+
+        // Allowed list
+        final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
+                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])),
+                allow);
+
+        // Denied list
+        final Set<Range<Integer>> disallow =
+                vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
+                        null /* allowedApplications */, Arrays.asList(packages));
+        assertEquals(rangeSet(
+                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)),
+                disallow);
+    }
+
+    @Test
+    public void testGetAlwaysAndOnGetLockDown() throws Exception {
+        final Vpn vpn = createVpn(primaryUser.id);
+
+        // Default state.
+        assertFalse(vpn.getAlwaysOn());
+        assertFalse(vpn.getLockdown());
+
+        // Set always-on without lockdown.
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList()));
+        assertTrue(vpn.getAlwaysOn());
+        assertFalse(vpn.getLockdown());
+
+        // Set always-on with lockdown.
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList()));
+        assertTrue(vpn.getAlwaysOn());
+        assertTrue(vpn.getLockdown());
+
+        // Remove always-on configuration.
+        assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList()));
+        assertFalse(vpn.getAlwaysOn());
+        assertFalse(vpn.getLockdown());
+    }
+
+    @Test
+    public void testLockdownChangingPackage() throws Exception {
+        final Vpn vpn = createVpn(primaryUser.id);
+        final Range<Integer> user = PRI_USER_RANGE;
+        final int userStart = user.getLower();
+        final int userStop = user.getUpper();
+        // Set always-on without lockdown.
+        assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null));
+
+        // Set always-on with lockdown.
+        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)
+        }));
+
+        // 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)
+        }));
+        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
+                new UidRangeParcel(userStart, userStart + PKG_UIDS[3] - 1),
+                new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+        }));
+    }
+
+    @Test
+    public void testLockdownAllowlist() throws Exception {
+        final Vpn vpn = createVpn(primaryUser.id);
+        final Range<Integer> user = PRI_USER_RANGE;
+        final int userStart = user.getLower();
+        final int userStop = user.getUpper();
+        // Set always-on with lockdown and allow app PKGS[2] from lockdown.
+        assertTrue(vpn.setAlwaysOnPackage(
+                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)
+        }));
+        // 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)
+        }));
+        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)
+        }));
+
+        // Change the VPN app.
+        assertTrue(vpn.setAlwaysOnPackage(
+                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)
+        }));
+        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)
+        }));
+
+        // 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)
+        }));
+        verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
+                new UidRangeParcel(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)
+        }));
+        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)
+        }));
+
+        // Try allowing a package with a comma, should be rejected.
+        assertFalse(vpn.setAlwaysOnPackage(
+                PKGS[0], true, Collections.singletonList("a.b,c.d")));
+
+        // Pass a non-existent packages in the allowlist, they (and only they) should be ignored.
+        // allowed package should change from PGKS[1] to PKGS[2].
+        assertTrue(vpn.setAlwaysOnPackage(
+                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)
+        }));
+        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)
+        }));
+    }
+
+    @Test
+    public void testLockdownRuleRepeatability() throws Exception {
+        final Vpn vpn = createVpn(primaryUser.id);
+        final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
+                new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())};
+        // Given legacy lockdown is already enabled,
+        vpn.setLockdown(true);
+        verify(mConnectivityManager, times(1)).setRequireVpnForUids(true,
+                toRanges(primaryUserRangeParcel));
+
+        // Enabling legacy lockdown twice should do nothing.
+        vpn.setLockdown(true);
+        verify(mConnectivityManager, times(1)).setRequireVpnForUids(anyBoolean(), any());
+
+        // And disabling should remove the rules exactly once.
+        vpn.setLockdown(false);
+        verify(mConnectivityManager, times(1)).setRequireVpnForUids(false,
+                toRanges(primaryUserRangeParcel));
+
+        // Removing the lockdown again should have no effect.
+        vpn.setLockdown(false);
+        verify(mConnectivityManager, times(2)).setRequireVpnForUids(anyBoolean(), any());
+    }
+
+    private ArrayList<Range<Integer>> toRanges(UidRangeParcel[] ranges) {
+        ArrayList<Range<Integer>> rangesArray = new ArrayList<>(ranges.length);
+        for (int i = 0; i < ranges.length; i++) {
+            rangesArray.add(new Range<>(ranges[i].start, ranges[i].stop));
+        }
+        return rangesArray;
+    }
+
+    @Test
+    public void testLockdownRuleReversibility() throws Exception {
+        final Vpn vpn = createVpn(primaryUser.id);
+        final UidRangeParcel[] entireUser = {
+            new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())
+        };
+        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)
+        };
+
+        final InOrder order = inOrder(mConnectivityManager);
+
+        // Given lockdown is enabled with no package (legacy VPN),
+        vpn.setLockdown(true);
+        order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser));
+
+        // When a new VPN package is set the rules should change to cover that package.
+        vpn.prepare(null, PKGS[0], VpnManager.TYPE_VPN_SERVICE);
+        order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(entireUser));
+        order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(exceptPkg0));
+
+        // When that VPN package is unset, everything should be undone again in reverse.
+        vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE);
+        order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(exceptPkg0));
+        order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser));
+    }
+
+    @Test
+    public void testIsAlwaysOnPackageSupported() throws Exception {
+        final Vpn vpn = createVpn(primaryUser.id);
+
+        ApplicationInfo appInfo = new ApplicationInfo();
+        when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(primaryUser.id)))
+                .thenReturn(appInfo);
+
+        ServiceInfo svcInfo = new ServiceInfo();
+        ResolveInfo resInfo = new ResolveInfo();
+        resInfo.serviceInfo = svcInfo;
+        when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA),
+                eq(primaryUser.id)))
+                .thenReturn(Collections.singletonList(resInfo));
+
+        // null package name should return false
+        assertFalse(vpn.isAlwaysOnPackageSupported(null));
+
+        // Pre-N apps are not supported
+        appInfo.targetSdkVersion = VERSION_CODES.M;
+        assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0]));
+
+        // N+ apps are supported by default
+        appInfo.targetSdkVersion = VERSION_CODES.N;
+        assertTrue(vpn.isAlwaysOnPackageSupported(PKGS[0]));
+
+        // Apps that opt out explicitly are not supported
+        appInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT;
+        Bundle metaData = new Bundle();
+        metaData.putBoolean(VpnService.SERVICE_META_DATA_SUPPORTS_ALWAYS_ON, false);
+        svcInfo.metaData = metaData;
+        assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0]));
+    }
+
+    @Test
+    public void testNotificationShownForAlwaysOnApp() throws Exception {
+        final UserHandle userHandle = UserHandle.of(primaryUser.id);
+        final Vpn vpn = createVpn(primaryUser.id);
+        setMockedUsers(primaryUser);
+
+        final InOrder order = inOrder(mNotificationManager);
+
+        // Don't show a notification for regular disconnected states.
+        vpn.updateState(DetailedState.DISCONNECTED, TAG);
+        order.verify(mNotificationManager, atLeastOnce()).cancel(anyString(), anyInt());
+
+        // Start showing a notification for disconnected once always-on.
+        vpn.setAlwaysOnPackage(PKGS[0], false, null);
+        order.verify(mNotificationManager).notify(anyString(), anyInt(), any());
+
+        // Stop showing the notification once connected.
+        vpn.updateState(DetailedState.CONNECTED, TAG);
+        order.verify(mNotificationManager).cancel(anyString(), anyInt());
+
+        // Show the notification if we disconnect again.
+        vpn.updateState(DetailedState.DISCONNECTED, TAG);
+        order.verify(mNotificationManager).notify(anyString(), anyInt(), any());
+
+        // Notification should be cleared after unsetting always-on package.
+        vpn.setAlwaysOnPackage(null, false, null);
+        order.verify(mNotificationManager).cancel(anyString(), anyInt());
+    }
+
+    /**
+     * The profile name should NOT change between releases for backwards compatibility
+     *
+     * <p>If this is changed between releases, the {@link Vpn#getVpnProfilePrivileged()} method MUST
+     * be updated to ensure backward compatibility.
+     */
+    @Test
+    public void testGetProfileNameForPackage() throws Exception {
+        final Vpn vpn = createVpn(primaryUser.id);
+        setMockedUsers(primaryUser);
+
+        final String expected = Credentials.PLATFORM_VPN + primaryUser.id + "_" + TEST_VPN_PKG;
+        assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG));
+    }
+
+    private Vpn createVpnAndSetupUidChecks(String... grantedOps) throws Exception {
+        return createVpnAndSetupUidChecks(primaryUser, grantedOps);
+    }
+
+    private Vpn createVpnAndSetupUidChecks(UserInfo user, String... grantedOps) throws Exception {
+        final Vpn vpn = createVpn(user.id);
+        setMockedUsers(user);
+
+        when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt()))
+                .thenReturn(Process.myUid());
+
+        for (final String opStr : grantedOps) {
+            when(mAppOps.noteOpNoThrow(opStr, Process.myUid(), TEST_VPN_PKG,
+                    null /* attributionTag */, null /* message */))
+                    .thenReturn(AppOpsManager.MODE_ALLOWED);
+        }
+
+        return vpn;
+    }
+
+    private void checkProvisionVpnProfile(Vpn vpn, boolean expectedResult, String... checkedOps) {
+        assertEquals(expectedResult, vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile));
+
+        // The profile should always be stored, whether or not consent has been previously granted.
+        verify(mVpnProfileStore)
+                .put(
+                        eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)),
+                        eq(mVpnProfile.encode()));
+
+        for (final String checkedOpStr : checkedOps) {
+            verify(mAppOps).noteOpNoThrow(checkedOpStr, Process.myUid(), TEST_VPN_PKG,
+                    null /* attributionTag */, null /* message */);
+        }
+    }
+
+    @Test
+    public void testProvisionVpnProfileNoIpsecTunnels() throws Exception {
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS))
+                .thenReturn(false);
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+
+        try {
+            checkProvisionVpnProfile(
+                    vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+            fail("Expected exception due to missing feature");
+        } catch (UnsupportedOperationException expected) {
+        }
+    }
+
+    @Test
+    public void testProvisionVpnProfilePreconsented() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+
+        checkProvisionVpnProfile(
+                vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+    }
+
+    @Test
+    public void testProvisionVpnProfileNotPreconsented() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks();
+
+        // Expect that both the ACTIVATE_VPN and ACTIVATE_PLATFORM_VPN were tried, but the caller
+        // had neither.
+        checkProvisionVpnProfile(vpn, false /* expectedResult */,
+                AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN, AppOpsManager.OPSTR_ACTIVATE_VPN);
+    }
+
+    @Test
+    public void testProvisionVpnProfileVpnServicePreconsented() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_VPN);
+
+        checkProvisionVpnProfile(vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_VPN);
+    }
+
+    @Test
+    public void testProvisionVpnProfileTooLarge() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+
+        final VpnProfile bigProfile = new VpnProfile("");
+        bigProfile.name = new String(new byte[Vpn.MAX_VPN_PROFILE_SIZE_BYTES + 1]);
+
+        try {
+            vpn.provisionVpnProfile(TEST_VPN_PKG, bigProfile);
+            fail("Expected IAE due to profile size");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testProvisionVpnProfileRestrictedUser() throws Exception {
+        final Vpn vpn =
+                createVpnAndSetupUidChecks(
+                        restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+
+        try {
+            vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile);
+            fail("Expected SecurityException due to restricted user");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
+    public void testDeleteVpnProfile() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks();
+
+        vpn.deleteVpnProfile(TEST_VPN_PKG);
+
+        verify(mVpnProfileStore)
+                .remove(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
+    }
+
+    @Test
+    public void testDeleteVpnProfileRestrictedUser() throws Exception {
+        final Vpn vpn =
+                createVpnAndSetupUidChecks(
+                        restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+
+        try {
+            vpn.deleteVpnProfile(TEST_VPN_PKG);
+            fail("Expected SecurityException due to restricted user");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
+    public void testGetVpnProfilePrivileged() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks();
+
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(new VpnProfile("").encode());
+
+        vpn.getVpnProfilePrivileged(TEST_VPN_PKG);
+
+        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
+    }
+
+    @Test
+    public void testStartVpnProfile() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+
+        vpn.startVpnProfile(TEST_VPN_PKG);
+
+        verify(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 */);
+    }
+
+    @Test
+    public void testStartVpnProfileVpnServicePreconsented() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_VPN);
+
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+
+        vpn.startVpnProfile(TEST_VPN_PKG);
+
+        // Verify that the 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 */);
+    }
+
+    @Test
+    public void testStartVpnProfileNotConsented() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks();
+
+        try {
+            vpn.startVpnProfile(TEST_VPN_PKG);
+            fail("Expected failure due to no user consent");
+        } catch (SecurityException expected) {
+        }
+
+        // Verify both appops were checked.
+        verify(mAppOps)
+                .noteOpNoThrow(
+                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
+                        eq(Process.myUid()),
+                        eq(TEST_VPN_PKG),
+                        eq(null) /* attributionTag */,
+                        eq(null) /* message */);
+        verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(),
+                TEST_VPN_PKG, null /* attributionTag */, null /* message */);
+
+        // Keystore should never have been accessed.
+        verify(mVpnProfileStore, never()).get(any());
+    }
+
+    @Test
+    public void testStartVpnProfileMissingProfile() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))).thenReturn(null);
+
+        try {
+            vpn.startVpnProfile(TEST_VPN_PKG);
+            fail("Expected failure due to missing profile");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        verify(mVpnProfileStore).get(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 */);
+    }
+
+    @Test
+    public void testStartVpnProfileRestrictedUser() throws Exception {
+        final Vpn vpn =
+                createVpnAndSetupUidChecks(
+                        restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+
+        try {
+            vpn.startVpnProfile(TEST_VPN_PKG);
+            fail("Expected SecurityException due to restricted user");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
+    public void testStopVpnProfileRestrictedUser() throws Exception {
+        final Vpn vpn =
+                createVpnAndSetupUidChecks(
+                        restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+
+        try {
+            vpn.stopVpnProfile(TEST_VPN_PKG);
+            fail("Expected SecurityException due to restricted user");
+        } catch (SecurityException expected) {
+        }
+    }
+
+    @Test
+    public void testSetPackageAuthorizationVpnService() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks();
+
+        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_SERVICE));
+        verify(mAppOps)
+                .setMode(
+                        eq(AppOpsManager.OPSTR_ACTIVATE_VPN),
+                        eq(Process.myUid()),
+                        eq(TEST_VPN_PKG),
+                        eq(AppOpsManager.MODE_ALLOWED));
+    }
+
+    @Test
+    public void testSetPackageAuthorizationPlatformVpn() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks();
+
+        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_PLATFORM));
+        verify(mAppOps)
+                .setMode(
+                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
+                        eq(Process.myUid()),
+                        eq(TEST_VPN_PKG),
+                        eq(AppOpsManager.MODE_ALLOWED));
+    }
+
+    @Test
+    public void testSetPackageAuthorizationRevokeAuthorization() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks();
+
+        assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_NONE));
+        verify(mAppOps)
+                .setMode(
+                        eq(AppOpsManager.OPSTR_ACTIVATE_VPN),
+                        eq(Process.myUid()),
+                        eq(TEST_VPN_PKG),
+                        eq(AppOpsManager.MODE_IGNORED));
+        verify(mAppOps)
+                .setMode(
+                        eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
+                        eq(Process.myUid()),
+                        eq(TEST_VPN_PKG),
+                        eq(AppOpsManager.MODE_IGNORED));
+    }
+
+    private NetworkCallback triggerOnAvailableAndGetCallback() throws Exception {
+        final ArgumentCaptor<NetworkCallback> networkCallbackCaptor =
+                ArgumentCaptor.forClass(NetworkCallback.class);
+        verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
+                .requestNetwork(any(), networkCallbackCaptor.capture());
+
+        // onAvailable() will trigger onDefaultNetworkChanged(), so NetdUtils#setInterfaceUp will be
+        // invoked. Set the return value of INetd#interfaceGetCfg to prevent NullPointerException.
+        final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel();
+        config.flags = new String[] {IF_STATE_DOWN};
+        when(mNetd.interfaceGetCfg(anyString())).thenReturn(config);
+        final NetworkCallback cb = networkCallbackCaptor.getValue();
+        cb.onAvailable(TEST_NETWORK);
+        return cb;
+    }
+
+    private void verifyInterfaceSetCfgWithFlags(String flag) throws Exception {
+        // Add a timeout for waiting for interfaceSetCfg to be called.
+        verify(mNetd, timeout(TEST_TIMEOUT_MS)).interfaceSetCfg(argThat(
+                config -> Arrays.asList(config.flags).contains(flag)));
+    }
+
+    @Test
+    public void testStartPlatformVpnAuthenticationFailed() 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 NetworkCallback cb = triggerOnAvailableAndGetCallback();
+
+        verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
+
+        // Wait for createIkeSession() to be called before proceeding in order to ensure consistent
+        // state
+        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
+                .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
+        final IkeSessionCallback ikeCb = captor.getValue();
+        ikeCb.onClosedExceptionally(exception);
+
+        verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb));
+        assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state);
+    }
+
+    @Test
+    public void testStartPlatformVpnIllegalArgumentExceptionInSetup() throws Exception {
+        when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
+                .thenThrow(new IllegalArgumentException());
+        final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), mVpnProfile);
+        final NetworkCallback cb = triggerOnAvailableAndGetCallback();
+
+        verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
+
+        // Wait for createIkeSession() to be called before proceeding in order to ensure consistent
+        // state
+        verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb));
+        assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state);
+    }
+
+    private void setAndVerifyAlwaysOnPackage(Vpn vpn, int uid, boolean lockdownEnabled) {
+        assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, lockdownEnabled, null));
+
+        verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
+        verify(mAppOps).setMode(
+                eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), eq(uid), eq(TEST_VPN_PKG),
+                eq(AppOpsManager.MODE_ALLOWED));
+
+        verify(mSystemServices).settingsSecurePutStringForUser(
+                eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(primaryUser.id));
+        verify(mSystemServices).settingsSecurePutIntForUser(
+                eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0),
+                eq(primaryUser.id));
+        verify(mSystemServices).settingsSecurePutStringForUser(
+                eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(primaryUser.id));
+    }
+
+    @Test
+    public void testSetAndStartAlwaysOnVpn() throws Exception {
+        final Vpn vpn = createVpn(primaryUser.id);
+        setMockedUsers(primaryUser);
+
+        // UID checks must return a different UID; otherwise it'll be treated as already prepared.
+        final int uid = Process.myUid() + 1;
+        when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt()))
+                .thenReturn(uid);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+
+        setAndVerifyAlwaysOnPackage(vpn, uid, false);
+        assertTrue(vpn.startAlwaysOnVpn());
+
+        // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in
+        // a subsequent CL.
+    }
+
+    private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
+        setMockedUsers(primaryUser);
+
+        // Dummy egress interface
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(EGRESS_IFACE);
+
+        final RouteInfo defaultRoute = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
+                        InetAddresses.parseNumericAddress("192.0.2.0"), EGRESS_IFACE);
+        lp.addRoute(defaultRoute);
+
+        vpn.startLegacyVpn(vpnProfile, EGRESS_NETWORK, lp);
+        return vpn;
+    }
+
+    @Test
+    public void testStartPlatformVpn() throws Exception {
+        startLegacyVpn(createVpn(primaryUser.id), mVpnProfile);
+        // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in
+        // a subsequent patch.
+    }
+
+    @Test
+    public void testStartRacoonNumericAddress() throws Exception {
+        startRacoon("1.2.3.4", "1.2.3.4");
+    }
+
+    @Test
+    public void testStartRacoonHostname() throws Exception {
+        startRacoon("hostname", "5.6.7.8"); // address returned by deps.resolve
+    }
+
+    private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
+        assertNotNull(nc);
+        VpnTransportInfo ti = (VpnTransportInfo) nc.getTransportInfo();
+        assertNotNull(ti);
+        assertEquals(type, ti.getType());
+    }
+
+    public void startRacoon(final String serverAddr, final String expectedAddr)
+            throws Exception {
+        final ConditionVariable legacyRunnerReady = new ConditionVariable();
+        final VpnProfile profile = new VpnProfile("testProfile" /* key */);
+        profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
+        profile.name = "testProfileName";
+        profile.username = "userName";
+        profile.password = "thePassword";
+        profile.server = serverAddr;
+        profile.ipsecIdentifier = "id";
+        profile.ipsecSecret = "secret";
+        profile.l2tpSecret = "l2tpsecret";
+
+        when(mConnectivityManager.getAllNetworks())
+            .thenReturn(new Network[] { new Network(101) });
+
+        when(mConnectivityManager.registerNetworkAgent(any(), any(), any(), any(),
+                any(), any(), anyInt())).thenAnswer(invocation -> {
+                    // The runner has registered an agent and is now ready.
+                    legacyRunnerReady.open();
+                    return new Network(102);
+                });
+        final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), profile);
+        final TestDeps deps = (TestDeps) vpn.mDeps;
+        try {
+            // udppsk and 1701 are the values for TYPE_L2TP_IPSEC_PSK
+            assertArrayEquals(
+                    new String[] { EGRESS_IFACE, expectedAddr, "udppsk",
+                            profile.ipsecIdentifier, profile.ipsecSecret, "1701" },
+                    deps.racoonArgs.get(10, TimeUnit.SECONDS));
+            // literal values are hardcoded in Vpn.java for mtpd args
+            assertArrayEquals(
+                    new String[] { EGRESS_IFACE, "l2tp", expectedAddr, "1701", profile.l2tpSecret,
+                            "name", profile.username, "password", profile.password,
+                            "linkname", "vpn", "refuse-eap", "nodefaultroute", "usepeerdns",
+                            "idle", "1800", "mtu", "1270", "mru", "1270" },
+                    deps.mtpdArgs.get(10, TimeUnit.SECONDS));
+
+            // Now wait for the runner to be ready before testing for the route.
+            ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
+            ArgumentCaptor<NetworkCapabilities> ncCaptor =
+                    ArgumentCaptor.forClass(NetworkCapabilities.class);
+            verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
+                    lpCaptor.capture(), ncCaptor.capture(), any(), any(), anyInt());
+
+            // In this test the expected address is always v4 so /32.
+            // Note that the interface needs to be specified because RouteInfo objects stored in
+            // LinkProperties objects always acquire the LinkProperties' interface.
+            final RouteInfo expectedRoute = new RouteInfo(new IpPrefix(expectedAddr + "/32"),
+                    null, EGRESS_IFACE, RouteInfo.RTN_THROW);
+            final List<RouteInfo> actualRoutes = lpCaptor.getValue().getRoutes();
+            assertTrue("Expected throw route (" + expectedRoute + ") not found in " + actualRoutes,
+                    actualRoutes.contains(expectedRoute));
+
+            assertTransportInfoMatches(ncCaptor.getValue(), VpnManager.TYPE_VPN_LEGACY);
+        } finally {
+            // Now interrupt the thread, unblock the runner and clean up.
+            vpn.mVpnRunner.exitVpnRunner();
+            deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
+            vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
+        }
+    }
+
+    private static final class TestDeps extends Vpn.Dependencies {
+        public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
+        public final CompletableFuture<String[]> mtpdArgs = new CompletableFuture();
+        public final File mStateFile;
+
+        private final HashMap<String, Boolean> mRunningServices = new HashMap<>();
+
+        TestDeps() {
+            try {
+                mStateFile = File.createTempFile("vpnTest", ".tmp");
+                mStateFile.deleteOnExit();
+            } catch (final IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public boolean isCallerSystem() {
+            return true;
+        }
+
+        @Override
+        public void startService(final String serviceName) {
+            mRunningServices.put(serviceName, true);
+        }
+
+        @Override
+        public void stopService(final String serviceName) {
+            mRunningServices.put(serviceName, false);
+        }
+
+        @Override
+        public boolean isServiceRunning(final String serviceName) {
+            return mRunningServices.getOrDefault(serviceName, false);
+        }
+
+        @Override
+        public boolean isServiceStopped(final String serviceName) {
+            return !isServiceRunning(serviceName);
+        }
+
+        @Override
+        public File getStateFile() {
+            return mStateFile;
+        }
+
+        @Override
+        public PendingIntent getIntentForStatusPanel(Context context) {
+            return null;
+        }
+
+        @Override
+        public void sendArgumentsToDaemon(
+                final String daemon, final LocalSocket socket, final String[] arguments,
+                final Vpn.RetryScheduler interruptChecker) throws IOException {
+            if ("racoon".equals(daemon)) {
+                racoonArgs.complete(arguments);
+            } else if ("mtpd".equals(daemon)) {
+                writeStateFile(arguments);
+                mtpdArgs.complete(arguments);
+            } else {
+                throw new UnsupportedOperationException("Unsupported daemon : " + daemon);
+            }
+        }
+
+        private void writeStateFile(final String[] arguments) throws IOException {
+            mStateFile.delete();
+            mStateFile.createNewFile();
+            mStateFile.deleteOnExit();
+            final BufferedWriter writer = new BufferedWriter(
+                    new FileWriter(mStateFile, false /* append */));
+            writer.write(EGRESS_IFACE);
+            writer.write("\n");
+            // addresses
+            writer.write("10.0.0.1/24\n");
+            // routes
+            writer.write("192.168.6.0/24\n");
+            // dns servers
+            writer.write("192.168.6.1\n");
+            // search domains
+            writer.write("vpn.searchdomains.com\n");
+            // endpoint - intentionally empty
+            writer.write("\n");
+            writer.flush();
+            writer.close();
+        }
+
+        @Override
+        @NonNull
+        public InetAddress resolve(final String endpoint) {
+            try {
+                // If a numeric IP address, return it.
+                return InetAddress.parseNumericAddress(endpoint);
+            } catch (IllegalArgumentException e) {
+                // Otherwise, return some token IP to test for.
+                return InetAddress.parseNumericAddress("5.6.7.8");
+            }
+        }
+
+        @Override
+        public boolean isInterfacePresent(final Vpn vpn, final String iface) {
+            return true;
+        }
+    }
+
+    /**
+     * Mock some methods of vpn object.
+     */
+    private Vpn createVpn(@UserIdInt int userId) {
+        final Context asUserContext = mock(Context.class, AdditionalAnswers.delegatesTo(mContext));
+        doReturn(UserHandle.of(userId)).when(asUserContext).getUser();
+        when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt()))
+                .thenReturn(asUserContext);
+        final TestLooper testLooper = new TestLooper();
+        final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, new TestDeps(), mNetService,
+                mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator);
+        verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat(
+                provider -> provider.getName().contains("VpnNetworkProvider")
+        ));
+        return vpn;
+    }
+
+    /**
+     * Populate {@link #mUserManager} with a list of fake users.
+     */
+    private void setMockedUsers(UserInfo... users) {
+        final Map<Integer, UserInfo> userMap = new ArrayMap<>();
+        for (UserInfo user : users) {
+            userMap.put(user.id, user);
+        }
+
+        /**
+         * @see UserManagerService#getUsers(boolean)
+         */
+        doAnswer(invocation -> {
+            final ArrayList<UserInfo> result = new ArrayList<>(users.length);
+            for (UserInfo ui : users) {
+                if (ui.isEnabled() && !ui.partial) {
+                    result.add(ui);
+                }
+            }
+            return result;
+        }).when(mUserManager).getAliveUsers();
+
+        doAnswer(invocation -> {
+            final int id = (int) invocation.getArguments()[0];
+            return userMap.get(id);
+        }).when(mUserManager).getUserInfo(anyInt());
+    }
+
+    /**
+     * Populate {@link #mPackageManager} with a fake packageName-to-UID mapping.
+     */
+    private void setMockedPackages(final Map<String, Integer> packages) {
+        try {
+            doAnswer(invocation -> {
+                final String appName = (String) invocation.getArguments()[0];
+                final int userId = (int) invocation.getArguments()[1];
+                Integer appId = packages.get(appName);
+                if (appId == null) throw new PackageManager.NameNotFoundException(appName);
+                return UserHandle.getUid(userId, appId);
+            }).when(mPackageManager).getPackageUidAsUser(anyString(), anyInt());
+        } catch (Exception e) {
+        }
+    }
+
+    private void setMockedNetworks(final Map<Network, NetworkCapabilities> networks) {
+        doAnswer(invocation -> {
+            final Network network = (Network) invocation.getArguments()[0];
+            return networks.get(network);
+        }).when(mConnectivityManager).getNetworkCapabilities(any());
+    }
+
+    // Need multiple copies of this, but Java's Stream objects can't be reused or
+    // duplicated.
+    private Stream<String> publicIpV4Routes() {
+        return Stream.of(
+                "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4",
+                "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6",
+                "172.0.0.0/12", "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9",
+                "173.0.0.0/8", "174.0.0.0/7", "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11",
+                "192.160.0.0/13", "192.169.0.0/16", "192.170.0.0/15", "192.172.0.0/14",
+                "192.176.0.0/12", "192.192.0.0/10", "193.0.0.0/8", "194.0.0.0/7",
+                "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4");
+    }
+
+    private Stream<String> publicIpV6Routes() {
+        return Stream.of(
+                "::/1", "8000::/2", "c000::/3", "e000::/4", "f000::/5", "f800::/6",
+                "fe00::/8", "2605:ef80:e:af1d::/64");
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java b/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java
new file mode 100644
index 0000000..8b730af
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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 com.android.server.net;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.Manifest.permission;
+import android.app.AppOpsManager;
+import android.app.admin.DevicePolicyManagerInternal;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.telephony.TelephonyManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.LocalServices;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkStatsAccessTest {
+    private static final String TEST_PKG = "com.example.test";
+    private static final int TEST_UID = 12345;
+
+    @Mock private Context mContext;
+    @Mock private DevicePolicyManagerInternal mDpmi;
+    @Mock private TelephonyManager mTm;
+    @Mock private AppOpsManager mAppOps;
+
+    // Hold the real service so we can restore it when tearing down the test.
+    private DevicePolicyManagerInternal mSystemDpmi;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mSystemDpmi = LocalServices.getService(DevicePolicyManagerInternal.class);
+        LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class);
+        LocalServices.addService(DevicePolicyManagerInternal.class, mDpmi);
+
+        when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTm);
+        when(mContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOps);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class);
+        LocalServices.addService(DevicePolicyManagerInternal.class, mSystemDpmi);
+    }
+
+    @Test
+    public void testCheckAccessLevel_hasCarrierPrivileges() throws Exception {
+        setHasCarrierPrivileges(true);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(false);
+        setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.DEVICE,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    @Test
+    public void testCheckAccessLevel_isDeviceOwner() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(true);
+        setIsProfileOwner(false);
+        setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.DEVICE,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    @Test
+    public void testCheckAccessLevel_isProfileOwner() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(true);
+        setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.USER,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    @Test
+    public void testCheckAccessLevel_hasAppOpsBitAllowed() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(true);
+        setHasAppOpsPermission(AppOpsManager.MODE_ALLOWED, false);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.DEVICESUMMARY,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    @Test
+    public void testCheckAccessLevel_hasAppOpsBitDefault_grantedPermission() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(true);
+        setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, true);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.DEVICESUMMARY,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    @Test
+    public void testCheckAccessLevel_hasReadHistoryPermission() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(true);
+        setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+        setHasReadHistoryPermission(true);
+        assertEquals(NetworkStatsAccess.Level.DEVICESUMMARY,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    @Test
+    public void testCheckAccessLevel_deniedAppOpsBit() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(false);
+        setHasAppOpsPermission(AppOpsManager.MODE_ERRORED, true);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.DEFAULT,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    @Test
+    public void testCheckAccessLevel_deniedAppOpsBit_deniedPermission() throws Exception {
+        setHasCarrierPrivileges(false);
+        setIsDeviceOwner(false);
+        setIsProfileOwner(false);
+        setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false);
+        setHasReadHistoryPermission(false);
+        assertEquals(NetworkStatsAccess.Level.DEFAULT,
+                NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG));
+    }
+
+    private void setHasCarrierPrivileges(boolean hasPrivileges) {
+        when(mTm.checkCarrierPrivilegesForPackageAnyPhone(TEST_PKG)).thenReturn(
+                hasPrivileges ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS
+                        : TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS);
+    }
+
+    private void setIsDeviceOwner(boolean isOwner) {
+        when(mDpmi.isActiveDeviceOwner(TEST_UID)).thenReturn(isOwner);
+    }
+
+    private void setIsProfileOwner(boolean isOwner) {
+        when(mDpmi.isActiveProfileOwner(TEST_UID)).thenReturn(isOwner);
+    }
+
+    private void setHasAppOpsPermission(int appOpsMode, boolean hasPermission) {
+        when(mAppOps.noteOp(AppOpsManager.OP_GET_USAGE_STATS, TEST_UID, TEST_PKG))
+                .thenReturn(appOpsMode);
+        when(mContext.checkCallingPermission(Manifest.permission.PACKAGE_USAGE_STATS)).thenReturn(
+                hasPermission ? PackageManager.PERMISSION_GRANTED
+                        : PackageManager.PERMISSION_DENIED);
+    }
+
+    private void setHasReadHistoryPermission(boolean hasPermission) {
+        when(mContext.checkCallingOrSelfPermission(permission.READ_NETWORK_USAGE_HISTORY))
+                .thenReturn(hasPermission ? PackageManager.PERMISSION_GRANTED
+                        : PackageManager.PERMISSION_DENIED);
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java b/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java
new file mode 100644
index 0000000..a058a46
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.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 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.TAG_NONE;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.NetworkStats;
+import android.net.UnderlyingNetworkInfo;
+
+import java.util.Arrays;
+
+/** Superclass with utilities for NetworkStats(Service|Factory)Test */
+abstract class NetworkStatsBaseTest {
+    static final String TEST_IFACE = "test0";
+    static final String TEST_IFACE2 = "test1";
+    static final String TUN_IFACE = "test_nss_tun0";
+    static final String TUN_IFACE2 = "test_nss_tun1";
+
+    static final int UID_RED = 1001;
+    static final int UID_BLUE = 1002;
+    static final int UID_GREEN = 1003;
+    static final int UID_VPN = 1004;
+
+    void assertValues(NetworkStats stats, String iface, int uid, long rxBytes,
+            long rxPackets, long txBytes, long txPackets) {
+        assertValues(
+                stats, iface, uid, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL,
+                rxBytes, rxPackets, txBytes, txPackets, 0);
+    }
+
+    void assertValues(NetworkStats stats, String iface, int uid, int set, int tag,
+            int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets,
+            long txBytes, long txPackets, long operations) {
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        final int[] sets;
+        if (set == SET_ALL) {
+            sets = new int[] {SET_ALL, SET_DEFAULT, SET_FOREGROUND};
+        } else {
+            sets = new int[] {set};
+        }
+
+        final int[] roamings;
+        if (roaming == ROAMING_ALL) {
+            roamings = new int[] {ROAMING_ALL, ROAMING_YES, ROAMING_NO};
+        } else {
+            roamings = new int[] {roaming};
+        }
+
+        final int[] meterings;
+        if (metered == METERED_ALL) {
+            meterings = new int[] {METERED_ALL, METERED_YES, METERED_NO};
+        } else {
+            meterings = new int[] {metered};
+        }
+
+        final int[] defaultNetworks;
+        if (defaultNetwork == DEFAULT_NETWORK_ALL) {
+            defaultNetworks =
+                    new int[] {DEFAULT_NETWORK_ALL, DEFAULT_NETWORK_YES, DEFAULT_NETWORK_NO};
+        } else {
+            defaultNetworks = new int[] {defaultNetwork};
+        }
+
+        for (int s : sets) {
+            for (int r : roamings) {
+                for (int m : meterings) {
+                    for (int d : defaultNetworks) {
+                        final int i = stats.findIndex(iface, uid, s, tag, m, r, d);
+                        if (i != -1) {
+                            entry.add(stats.getValues(i, null));
+                        }
+                    }
+                }
+            }
+        }
+
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+        assertEquals("unexpected txPackets", txPackets, entry.txPackets);
+        assertEquals("unexpected operations", operations, entry.operations);
+    }
+
+    static UnderlyingNetworkInfo createVpnInfo(String[] underlyingIfaces) {
+        return createVpnInfo(TUN_IFACE, underlyingIfaces);
+    }
+
+    static UnderlyingNetworkInfo createVpnInfo(String vpnIface, String[] underlyingIfaces) {
+        return new UnderlyingNetworkInfo(UID_VPN, vpnIface, Arrays.asList(underlyingIfaces));
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.java b/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.java
new file mode 100644
index 0000000..505ff9b
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.java
@@ -0,0 +1,594 @@
+/*
+ * 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.ConnectivityManager.TYPE_MOBILE;
+import static android.net.NetworkIdentity.OEM_NONE;
+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.NetworkStatsHistory.FIELD_ALL;
+import static android.net.NetworkTemplate.buildTemplateMobileAll;
+import static android.os.Process.myUid;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+
+import static com.android.internal.net.NetworkUtilsInternal.multiplySafeByRational;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.NetworkIdentity;
+import android.net.NetworkStats;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.os.Process;
+import android.os.UserHandle;
+import android.telephony.SubscriptionPlan;
+import android.telephony.TelephonyManager;
+import android.text.format.DateUtils;
+import android.util.RecurrenceRule;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.frameworks.tests.net.R;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for {@link NetworkStatsCollection}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkStatsCollectionTest {
+
+    private static final String TEST_FILE = "test.bin";
+    private static final String TEST_IMSI = "310260000000000";
+
+    private static final long TIME_A = 1326088800000L; // UTC: Monday 9th January 2012 06:00:00 AM
+    private static final long TIME_B = 1326110400000L; // UTC: Monday 9th January 2012 12:00:00 PM
+    private static final long TIME_C = 1326132000000L; // UTC: Monday 9th January 2012 06:00:00 PM
+
+    private static Clock sOriginalClock;
+
+    @Before
+    public void setUp() throws Exception {
+        sOriginalClock = RecurrenceRule.sClock;
+        // ignore any device overlay while testing
+        NetworkTemplate.forceAllNetworkTypes();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        RecurrenceRule.sClock = sOriginalClock;
+        NetworkTemplate.resetForceAllNetworkTypes();
+    }
+
+    private void setClock(Instant instant) {
+        RecurrenceRule.sClock = Clock.fixed(instant, ZoneId.systemDefault());
+    }
+
+    @Test
+    public void testReadLegacyNetwork() throws Exception {
+        final File testFile =
+                new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE);
+        stageFile(R.raw.netstats_v1, testFile);
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+        collection.readLegacyNetwork(testFile);
+
+        // verify that history read correctly
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                636016770L, 709306L, 88038768L, 518836L, NetworkStatsAccess.Level.DEVICE);
+
+        // now export into a unified format
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        collection.write(bos);
+
+        // clear structure completely
+        collection.reset();
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                0L, 0L, 0L, 0L, NetworkStatsAccess.Level.DEVICE);
+
+        // and read back into structure, verifying that totals are same
+        collection.read(new ByteArrayInputStream(bos.toByteArray()));
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                636016770L, 709306L, 88038768L, 518836L, NetworkStatsAccess.Level.DEVICE);
+    }
+
+    @Test
+    public void testReadLegacyUid() throws Exception {
+        final File testFile =
+                new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE);
+        stageFile(R.raw.netstats_uid_v4, testFile);
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+        collection.readLegacyUid(testFile, false);
+
+        // verify that history read correctly
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                637076152L, 711413L, 88343717L, 521022L, NetworkStatsAccess.Level.DEVICE);
+
+        // now export into a unified format
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        collection.write(bos);
+
+        // clear structure completely
+        collection.reset();
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                0L, 0L, 0L, 0L, NetworkStatsAccess.Level.DEVICE);
+
+        // and read back into structure, verifying that totals are same
+        collection.read(new ByteArrayInputStream(bos.toByteArray()));
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI),
+                637076152L, 711413L, 88343717L, 521022L, NetworkStatsAccess.Level.DEVICE);
+    }
+
+    @Test
+    public void testReadLegacyUidTags() throws Exception {
+        final File testFile =
+                new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE);
+        stageFile(R.raw.netstats_uid_v4, testFile);
+
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+        collection.readLegacyUid(testFile, true);
+
+        // verify that history read correctly
+        assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI),
+                77017831L, 100995L, 35436758L, 92344L);
+
+        // now export into a unified format
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        collection.write(bos);
+
+        // clear structure completely
+        collection.reset();
+        assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI),
+                0L, 0L, 0L, 0L);
+
+        // and read back into structure, verifying that totals are same
+        collection.read(new ByteArrayInputStream(bos.toByteArray()));
+        assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI),
+                77017831L, 100995L, 35436758L, 92344L);
+    }
+
+    @Test
+    public void testStartEndAtomicBuckets() throws Exception {
+        final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS);
+
+        // record empty data straddling between buckets
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        entry.rxBytes = 32;
+        collection.recordData(null, UID_ALL, SET_DEFAULT, TAG_NONE, 30 * MINUTE_IN_MILLIS,
+                90 * MINUTE_IN_MILLIS, entry);
+
+        // assert that we report boundary in atomic buckets
+        assertEquals(0, collection.getStartMillis());
+        assertEquals(2 * HOUR_IN_MILLIS, collection.getEndMillis());
+    }
+
+    @Test
+    public void testAccessLevels() throws Exception {
+        final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS);
+        final NetworkStats.Entry entry = new NetworkStats.Entry();
+        final NetworkIdentitySet identSet = new NetworkIdentitySet();
+        identSet.add(new NetworkIdentity(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN,
+                TEST_IMSI, null, false, true, true, OEM_NONE));
+
+        int myUid = Process.myUid();
+        int otherUidInSameUser = Process.myUid() + 1;
+        int uidInDifferentUser = Process.myUid() + UserHandle.PER_USER_RANGE;
+
+        // Record one entry for the current UID.
+        entry.rxBytes = 32;
+        collection.recordData(identSet, myUid, SET_DEFAULT, TAG_NONE, 0, 60 * MINUTE_IN_MILLIS,
+                entry);
+
+        // Record one entry for another UID in this user.
+        entry.rxBytes = 64;
+        collection.recordData(identSet, otherUidInSameUser, SET_DEFAULT, TAG_NONE, 0,
+                60 * MINUTE_IN_MILLIS, entry);
+
+        // Record one entry for the system UID.
+        entry.rxBytes = 128;
+        collection.recordData(identSet, Process.SYSTEM_UID, SET_DEFAULT, TAG_NONE, 0,
+                60 * MINUTE_IN_MILLIS, entry);
+
+        // Record one entry for a UID in a different user.
+        entry.rxBytes = 256;
+        collection.recordData(identSet, uidInDifferentUser, SET_DEFAULT, TAG_NONE, 0,
+                60 * MINUTE_IN_MILLIS, entry);
+
+        // Verify the set of relevant UIDs for each access level.
+        assertArrayEquals(new int[] { myUid },
+                collection.getRelevantUids(NetworkStatsAccess.Level.DEFAULT));
+        assertArrayEquals(new int[] { Process.SYSTEM_UID, myUid, otherUidInSameUser },
+                collection.getRelevantUids(NetworkStatsAccess.Level.USER));
+        assertArrayEquals(
+                new int[] { Process.SYSTEM_UID, myUid, otherUidInSameUser, uidInDifferentUser },
+                collection.getRelevantUids(NetworkStatsAccess.Level.DEVICE));
+
+        // Verify security check in getHistory.
+        assertNotNull(collection.getHistory(buildTemplateMobileAll(TEST_IMSI), null, myUid, SET_DEFAULT,
+                TAG_NONE, 0, 0L, 0L, NetworkStatsAccess.Level.DEFAULT, myUid));
+        try {
+            collection.getHistory(buildTemplateMobileAll(TEST_IMSI), null, otherUidInSameUser,
+                    SET_DEFAULT, TAG_NONE, 0, 0L, 0L, NetworkStatsAccess.Level.DEFAULT, myUid);
+            fail("Should have thrown SecurityException for accessing different UID");
+        } catch (SecurityException e) {
+            // expected
+        }
+
+        // Verify appropriate aggregation in getSummary.
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32, 0, 0, 0,
+                NetworkStatsAccess.Level.DEFAULT);
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32 + 64 + 128, 0, 0, 0,
+                NetworkStatsAccess.Level.USER);
+        assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32 + 64 + 128 + 256, 0, 0,
+                0, NetworkStatsAccess.Level.DEVICE);
+    }
+
+    @Test
+    public void testAugmentPlan() throws Exception {
+        final File testFile =
+                new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE);
+        stageFile(R.raw.netstats_v1, testFile);
+
+        final NetworkStatsCollection emptyCollection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+        final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+        collection.readLegacyNetwork(testFile);
+
+        // We're in the future, but not that far off
+        setClock(Instant.parse("2012-06-01T00:00:00.00Z"));
+
+        // Test a bunch of plans that should result in no augmentation
+        final List<SubscriptionPlan> plans = new ArrayList<>();
+
+        // No plan
+        plans.add(null);
+        // No usage anchor
+        plans.add(SubscriptionPlan.Builder
+                .createRecurringMonthly(ZonedDateTime.parse("2011-01-14T00:00:00.00Z")).build());
+        // Usage anchor far in past
+        plans.add(SubscriptionPlan.Builder
+                .createRecurringMonthly(ZonedDateTime.parse("2011-01-14T00:00:00.00Z"))
+                .setDataUsage(1000L, TIME_A - DateUtils.YEAR_IN_MILLIS).build());
+        // Usage anchor far in future
+        plans.add(SubscriptionPlan.Builder
+                .createRecurringMonthly(ZonedDateTime.parse("2011-01-14T00:00:00.00Z"))
+                .setDataUsage(1000L, TIME_A + DateUtils.YEAR_IN_MILLIS).build());
+        // Usage anchor near but outside cycle
+        plans.add(SubscriptionPlan.Builder
+                .createNonrecurring(ZonedDateTime.parse("2012-01-09T09:00:00.00Z"),
+                        ZonedDateTime.parse("2012-01-09T15:00:00.00Z"))
+                .setDataUsage(1000L, TIME_C).build());
+
+        for (SubscriptionPlan plan : plans) {
+            int i;
+            NetworkStatsHistory history;
+
+            // Empty collection should be untouched
+            history = getHistory(emptyCollection, plan, TIME_A, TIME_C);
+            assertEquals(0L, history.getTotalBytes());
+
+            // Normal collection should be untouched
+            history = getHistory(collection, plan, TIME_A, TIME_C); i = 0;
+            assertEntry(100647, 197, 23649, 185, history.getValues(i++, null));
+            assertEntry(100647, 196, 23648, 185, history.getValues(i++, null));
+            assertEntry(18323, 76, 15032, 76, history.getValues(i++, null));
+            assertEntry(18322, 75, 15031, 75, history.getValues(i++, null));
+            assertEntry(527798, 761, 78570, 652, history.getValues(i++, null));
+            assertEntry(527797, 760, 78570, 651, history.getValues(i++, null));
+            assertEntry(10747, 50, 16839, 55, history.getValues(i++, null));
+            assertEntry(10747, 49, 16837, 54, history.getValues(i++, null));
+            assertEntry(89191, 151, 18021, 140, history.getValues(i++, null));
+            assertEntry(89190, 150, 18020, 139, history.getValues(i++, null));
+            assertEntry(3821, 23, 4525, 26, history.getValues(i++, null));
+            assertEntry(3820, 21, 4524, 26, history.getValues(i++, null));
+            assertEntry(91686, 159, 18576, 146, history.getValues(i++, null));
+            assertEntry(91685, 159, 18574, 146, history.getValues(i++, null));
+            assertEntry(8289, 36, 6864, 39, history.getValues(i++, null));
+            assertEntry(8289, 34, 6862, 37, history.getValues(i++, null));
+            assertEntry(113914, 174, 18364, 157, history.getValues(i++, null));
+            assertEntry(113913, 173, 18364, 157, history.getValues(i++, null));
+            assertEntry(11378, 49, 9261, 50, history.getValues(i++, null));
+            assertEntry(11377, 48, 9261, 48, history.getValues(i++, null));
+            assertEntry(201766, 328, 41808, 291, history.getValues(i++, null));
+            assertEntry(201764, 328, 41807, 290, history.getValues(i++, null));
+            assertEntry(106106, 219, 39918, 202, history.getValues(i++, null));
+            assertEntry(106105, 216, 39916, 200, history.getValues(i++, null));
+            assertEquals(history.size(), i);
+
+            // Slice from middle should be untouched
+            history = getHistory(collection, plan, TIME_B - HOUR_IN_MILLIS,
+                    TIME_B + HOUR_IN_MILLIS); i = 0;
+            assertEntry(3821, 23, 4525, 26, history.getValues(i++, null));
+            assertEntry(3820, 21, 4524, 26, history.getValues(i++, null));
+            assertEntry(91686, 159, 18576, 146, history.getValues(i++, null));
+            assertEntry(91685, 159, 18574, 146, history.getValues(i++, null));
+            assertEquals(history.size(), i);
+        }
+
+        // Lower anchor in the middle of plan
+        {
+            int i;
+            NetworkStatsHistory history;
+
+            final SubscriptionPlan plan = SubscriptionPlan.Builder
+                    .createNonrecurring(ZonedDateTime.parse("2012-01-09T09:00:00.00Z"),
+                            ZonedDateTime.parse("2012-01-09T15:00:00.00Z"))
+                    .setDataUsage(200000L, TIME_B).build();
+
+            // Empty collection should be augmented
+            history = getHistory(emptyCollection, plan, TIME_A, TIME_C);
+            assertEquals(200000L, history.getTotalBytes());
+
+            // Normal collection should be augmented
+            history = getHistory(collection, plan, TIME_A, TIME_C); i = 0;
+            assertEntry(100647, 197, 23649, 185, history.getValues(i++, null));
+            assertEntry(100647, 196, 23648, 185, history.getValues(i++, null));
+            assertEntry(18323, 76, 15032, 76, history.getValues(i++, null));
+            assertEntry(18322, 75, 15031, 75, history.getValues(i++, null));
+            assertEntry(527798, 761, 78570, 652, history.getValues(i++, null));
+            assertEntry(527797, 760, 78570, 651, history.getValues(i++, null));
+            // Cycle point; start data normalization
+            assertEntry(7507, 0, 11763, 0, history.getValues(i++, null));
+            assertEntry(7507, 0, 11762, 0, history.getValues(i++, null));
+            assertEntry(62309, 0, 12589, 0, history.getValues(i++, null));
+            assertEntry(62309, 0, 12588, 0, history.getValues(i++, null));
+            assertEntry(2669, 0, 3161, 0, history.getValues(i++, null));
+            assertEntry(2668, 0, 3160, 0, history.getValues(i++, null));
+            // Anchor point; end data normalization
+            assertEntry(91686, 159, 18576, 146, history.getValues(i++, null));
+            assertEntry(91685, 159, 18574, 146, history.getValues(i++, null));
+            assertEntry(8289, 36, 6864, 39, history.getValues(i++, null));
+            assertEntry(8289, 34, 6862, 37, history.getValues(i++, null));
+            assertEntry(113914, 174, 18364, 157, history.getValues(i++, null));
+            assertEntry(113913, 173, 18364, 157, history.getValues(i++, null));
+            // Cycle point
+            assertEntry(11378, 49, 9261, 50, history.getValues(i++, null));
+            assertEntry(11377, 48, 9261, 48, history.getValues(i++, null));
+            assertEntry(201766, 328, 41808, 291, history.getValues(i++, null));
+            assertEntry(201764, 328, 41807, 290, history.getValues(i++, null));
+            assertEntry(106106, 219, 39918, 202, history.getValues(i++, null));
+            assertEntry(106105, 216, 39916, 200, history.getValues(i++, null));
+            assertEquals(history.size(), i);
+
+            // Slice from middle should be augmented
+            history = getHistory(collection, plan, TIME_B - HOUR_IN_MILLIS,
+                    TIME_B + HOUR_IN_MILLIS); i = 0;
+            assertEntry(2669, 0, 3161, 0, history.getValues(i++, null));
+            assertEntry(2668, 0, 3160, 0, history.getValues(i++, null));
+            assertEntry(91686, 159, 18576, 146, history.getValues(i++, null));
+            assertEntry(91685, 159, 18574, 146, history.getValues(i++, null));
+            assertEquals(history.size(), i);
+        }
+
+        // Higher anchor in the middle of plan
+        {
+            int i;
+            NetworkStatsHistory history;
+
+            final SubscriptionPlan plan = SubscriptionPlan.Builder
+                    .createNonrecurring(ZonedDateTime.parse("2012-01-09T09:00:00.00Z"),
+                            ZonedDateTime.parse("2012-01-09T15:00:00.00Z"))
+                    .setDataUsage(400000L, TIME_B + MINUTE_IN_MILLIS).build();
+
+            // Empty collection should be augmented
+            history = getHistory(emptyCollection, plan, TIME_A, TIME_C);
+            assertEquals(400000L, history.getTotalBytes());
+
+            // Normal collection should be augmented
+            history = getHistory(collection, plan, TIME_A, TIME_C); i = 0;
+            assertEntry(100647, 197, 23649, 185, history.getValues(i++, null));
+            assertEntry(100647, 196, 23648, 185, history.getValues(i++, null));
+            assertEntry(18323, 76, 15032, 76, history.getValues(i++, null));
+            assertEntry(18322, 75, 15031, 75, history.getValues(i++, null));
+            assertEntry(527798, 761, 78570, 652, history.getValues(i++, null));
+            assertEntry(527797, 760, 78570, 651, history.getValues(i++, null));
+            // Cycle point; start data normalization
+            assertEntry(15015, 0, 23527, 0, history.getValues(i++, null));
+            assertEntry(15015, 0, 23524, 0, history.getValues(i++, null));
+            assertEntry(124619, 0, 25179, 0, history.getValues(i++, null));
+            assertEntry(124618, 0, 25177, 0, history.getValues(i++, null));
+            assertEntry(5338, 0, 6322, 0, history.getValues(i++, null));
+            assertEntry(5337, 0, 6320, 0, history.getValues(i++, null));
+            // Anchor point; end data normalization
+            assertEntry(91686, 159, 18576, 146, history.getValues(i++, null));
+            assertEntry(91685, 159, 18574, 146, history.getValues(i++, null));
+            assertEntry(8289, 36, 6864, 39, history.getValues(i++, null));
+            assertEntry(8289, 34, 6862, 37, history.getValues(i++, null));
+            assertEntry(113914, 174, 18364, 157, history.getValues(i++, null));
+            assertEntry(113913, 173, 18364, 157, history.getValues(i++, null));
+            // Cycle point
+            assertEntry(11378, 49, 9261, 50, history.getValues(i++, null));
+            assertEntry(11377, 48, 9261, 48, history.getValues(i++, null));
+            assertEntry(201766, 328, 41808, 291, history.getValues(i++, null));
+            assertEntry(201764, 328, 41807, 290, history.getValues(i++, null));
+            assertEntry(106106, 219, 39918, 202, history.getValues(i++, null));
+            assertEntry(106105, 216, 39916, 200, history.getValues(i++, null));
+
+            // Slice from middle should be augmented
+            history = getHistory(collection, plan, TIME_B - HOUR_IN_MILLIS,
+                    TIME_B + HOUR_IN_MILLIS); i = 0;
+            assertEntry(5338, 0, 6322, 0, history.getValues(i++, null));
+            assertEntry(5337, 0, 6320, 0, history.getValues(i++, null));
+            assertEntry(91686, 159, 18576, 146, history.getValues(i++, null));
+            assertEntry(91685, 159, 18574, 146, history.getValues(i++, null));
+            assertEquals(history.size(), i);
+        }
+    }
+
+    @Test
+    public void testAugmentPlanGigantic() throws Exception {
+        // We're in the future, but not that far off
+        setClock(Instant.parse("2012-06-01T00:00:00.00Z"));
+
+        // Create a simple history with a ton of measured usage
+        final NetworkStatsCollection large = new NetworkStatsCollection(HOUR_IN_MILLIS);
+        final NetworkIdentitySet ident = new NetworkIdentitySet();
+        ident.add(new NetworkIdentity(ConnectivityManager.TYPE_MOBILE, -1, TEST_IMSI, null,
+                false, true, true, OEM_NONE));
+        large.recordData(ident, UID_ALL, SET_ALL, TAG_NONE, TIME_A, TIME_B,
+                new NetworkStats.Entry(12_730_893_164L, 1, 0, 0, 0));
+
+        // Verify untouched total
+        assertEquals(12_730_893_164L, getHistory(large, null, TIME_A, TIME_C).getTotalBytes());
+
+        // Verify anchor that might cause overflows
+        final SubscriptionPlan plan = SubscriptionPlan.Builder
+                .createRecurringMonthly(ZonedDateTime.parse("2012-01-09T00:00:00.00Z"))
+                .setDataUsage(4_939_212_390L, TIME_B).build();
+        assertEquals(4_939_212_386L, getHistory(large, plan, TIME_A, TIME_C).getTotalBytes());
+    }
+
+    @Test
+    public void testRounding() throws Exception {
+        final NetworkStatsCollection coll = new NetworkStatsCollection(HOUR_IN_MILLIS);
+
+        // Special values should remain unchanged
+        for (long time : new long[] {
+                Long.MIN_VALUE, Long.MAX_VALUE, SubscriptionPlan.TIME_UNKNOWN
+        }) {
+            assertEquals(time, coll.roundUp(time));
+            assertEquals(time, coll.roundDown(time));
+        }
+
+        assertEquals(TIME_A, coll.roundUp(TIME_A));
+        assertEquals(TIME_A, coll.roundDown(TIME_A));
+
+        assertEquals(TIME_A + HOUR_IN_MILLIS, coll.roundUp(TIME_A + 1));
+        assertEquals(TIME_A, coll.roundDown(TIME_A + 1));
+
+        assertEquals(TIME_A, coll.roundUp(TIME_A - 1));
+        assertEquals(TIME_A - HOUR_IN_MILLIS, coll.roundDown(TIME_A - 1));
+    }
+
+    @Test
+    public void testMultiplySafeRational() {
+        assertEquals(25, multiplySafeByRational(50, 1, 2));
+        assertEquals(100, multiplySafeByRational(50, 2, 1));
+
+        assertEquals(-10, multiplySafeByRational(30, -1, 3));
+        assertEquals(0, multiplySafeByRational(30, 0, 3));
+        assertEquals(10, multiplySafeByRational(30, 1, 3));
+        assertEquals(20, multiplySafeByRational(30, 2, 3));
+        assertEquals(30, multiplySafeByRational(30, 3, 3));
+        assertEquals(40, multiplySafeByRational(30, 4, 3));
+
+        assertEquals(100_000_000_000L,
+                multiplySafeByRational(300_000_000_000L, 10_000_000_000L, 30_000_000_000L));
+        assertEquals(100_000_000_010L,
+                multiplySafeByRational(300_000_000_000L, 10_000_000_001L, 30_000_000_000L));
+        assertEquals(823_202_048L,
+                multiplySafeByRational(4_939_212_288L, 2_121_815_528L, 12_730_893_165L));
+
+        assertThrows(ArithmeticException.class, () -> multiplySafeByRational(30, 3, 0));
+    }
+
+    /**
+     * Copy a {@link Resources#openRawResource(int)} into {@link File} for
+     * testing purposes.
+     */
+    private void stageFile(int rawId, File file) throws Exception {
+        new File(file.getParent()).mkdirs();
+        InputStream in = null;
+        OutputStream out = null;
+        try {
+            in = InstrumentationRegistry.getContext().getResources().openRawResource(rawId);
+            out = new FileOutputStream(file);
+            Streams.copy(in, out);
+        } finally {
+            IoUtils.closeQuietly(in);
+            IoUtils.closeQuietly(out);
+        }
+    }
+
+    private static NetworkStatsHistory getHistory(NetworkStatsCollection collection,
+            SubscriptionPlan augmentPlan, long start, long end) {
+        return collection.getHistory(buildTemplateMobileAll(TEST_IMSI), augmentPlan, UID_ALL,
+                SET_ALL, TAG_NONE, FIELD_ALL, start, end, NetworkStatsAccess.Level.DEVICE, myUid());
+    }
+
+    private static void assertSummaryTotal(NetworkStatsCollection collection,
+            NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets,
+            @NetworkStatsAccess.Level int accessLevel) {
+        final NetworkStats.Entry actual = collection.getSummary(
+                template, Long.MIN_VALUE, Long.MAX_VALUE, accessLevel, myUid())
+                .getTotal(null);
+        assertEntry(rxBytes, rxPackets, txBytes, txPackets, actual);
+    }
+
+    private static void assertSummaryTotalIncludingTags(NetworkStatsCollection collection,
+            NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+        final NetworkStats.Entry actual = collection.getSummary(
+                template, Long.MIN_VALUE, Long.MAX_VALUE, NetworkStatsAccess.Level.DEVICE, myUid())
+                .getTotalIncludingTags(null);
+        assertEntry(rxBytes, rxPackets, txBytes, txPackets, actual);
+    }
+
+    private static void assertEntry(long rxBytes, long rxPackets, long txBytes, long txPackets,
+            NetworkStats.Entry actual) {
+        assertEntry(new NetworkStats.Entry(rxBytes, rxPackets, txBytes, txPackets, 0L), actual);
+    }
+
+    private static void assertEntry(long rxBytes, long rxPackets, long txBytes, long txPackets,
+            NetworkStatsHistory.Entry actual) {
+        assertEntry(new NetworkStats.Entry(rxBytes, rxPackets, txBytes, txPackets, 0L), actual);
+    }
+
+    private static void assertEntry(NetworkStats.Entry expected,
+            NetworkStatsHistory.Entry actual) {
+        assertEntry(expected, new NetworkStats.Entry(actual.rxBytes, actual.rxPackets,
+                actual.txBytes, actual.txPackets, 0L));
+    }
+
+    private static void assertEntry(NetworkStats.Entry expected,
+            NetworkStats.Entry actual) {
+        assertEquals("unexpected rxBytes", expected.rxBytes, actual.rxBytes);
+        assertEquals("unexpected rxPackets", expected.rxPackets, actual.rxPackets);
+        assertEquals("unexpected txBytes", expected.txBytes, actual.txBytes);
+        assertEquals("unexpected txPackets", expected.txPackets, actual.txPackets);
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
new file mode 100644
index 0000000..40d4446
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -0,0 +1,572 @@
+/*
+ * 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.DEFAULT_NETWORK_ALL;
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+import static android.net.NetworkStats.METERED_ALL;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.ROAMING_ALL;
+import static android.net.NetworkStats.ROAMING_NO;
+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.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+
+import static com.android.server.NetworkManagementSocketTagger.kernelToTag;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.content.res.Resources;
+import android.net.NetworkStats;
+import android.net.TrafficStats;
+import android.net.UnderlyingNetworkInfo;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.frameworks.tests.net.R;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+import libcore.testing.io.TestIoUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/** Tests for {@link NetworkStatsFactory}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkStatsFactoryTest extends NetworkStatsBaseTest {
+    private static final String CLAT_PREFIX = "v4-";
+
+    private File mTestProc;
+    private NetworkStatsFactory mFactory;
+
+    @Before
+    public void setUp() throws Exception {
+        mTestProc = TestIoUtils.createTemporaryDirectory("proc");
+
+        // The libandroid_servers which have the native method is not available to
+        // applications. So in order to have a test support native library, the native code
+        // related to networkStatsFactory is compiled to a minimal native library and loaded here.
+        System.loadLibrary("networkstatsfactorytestjni");
+        mFactory = new NetworkStatsFactory(mTestProc, false);
+        mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mFactory = null;
+    }
+
+    @Test
+    public void testNetworkStatsDetail() throws Exception {
+        final NetworkStats stats = parseDetailedStats(R.raw.xt_qtaguid_typical);
+
+        assertEquals(70, stats.size());
+        assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, 18621L, 2898L);
+        assertStatsEntry(stats, "wlan0", 10011, SET_DEFAULT, 0x0, 35777L, 5718L);
+        assertStatsEntry(stats, "wlan0", 10021, SET_DEFAULT, 0x7fffff01, 562386L, 49228L);
+        assertStatsEntry(stats, "rmnet1", 10021, SET_DEFAULT, 0x30100000, 219110L, 227423L);
+        assertStatsEntry(stats, "rmnet2", 10001, SET_DEFAULT, 0x0, 1125899906842624L, 984L);
+    }
+
+    @Test
+    public void testVpnRewriteTrafficThroughItself() throws Exception {
+        UnderlyingNetworkInfo[] underlyingNetworkInfos =
+                new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos);
+
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        //
+        // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED
+        // over VPN.
+        // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE
+        // over VPN.
+        //
+        // VPN UID rewrites packets read from TUN back to TUN, plus some of its own traffic
+
+        final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_rewrite_through_self);
+
+        assertValues(tunStats, TUN_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 2000L, 200L, 1000L, 100L, 0);
+        assertValues(tunStats, TUN_IFACE, UID_BLUE, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 1000L, 100L, 500L, 50L, 0);
+        assertValues(tunStats, TUN_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 0L, 0L, 1600L, 160L, 0);
+
+        assertValues(tunStats, TEST_IFACE, UID_RED, 2000L, 200L, 1000L, 100L);
+        assertValues(tunStats, TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L);
+        assertValues(tunStats, TEST_IFACE, UID_VPN, 300L, 0L, 260L, 26L);
+    }
+
+    @Test
+    public void testVpnWithClat() throws Exception {
+        final UnderlyingNetworkInfo[] underlyingNetworkInfos = new UnderlyingNetworkInfo[] {
+                createVpnInfo(new String[] {CLAT_PREFIX + TEST_IFACE})};
+        mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos);
+        mFactory.noteStackedIface(CLAT_PREFIX + TEST_IFACE, TEST_IFACE);
+
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED
+        // over VPN.
+        // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE
+        // over VPN.
+        // VPN sent 1650 bytes (150 packets), and received 3300 (300 packets) over v4-WiFi, and clat
+        // added 20 bytes per packet of extra overhead
+        //
+        // For 1650 bytes sent over v4-WiFi, 4650 bytes were actually sent over WiFi, which is
+        // expected to be split as follows:
+        // UID_RED: 1000 bytes, 100 packets
+        // UID_BLUE: 500 bytes, 50 packets
+        // UID_VPN: 3150 bytes, 0 packets
+        //
+        // For 3300 bytes received over v4-WiFi, 9300 bytes were actually sent over WiFi, which is
+        // expected to be split as follows:
+        // UID_RED: 2000 bytes, 200 packets
+        // UID_BLUE: 1000 bytes, 100 packets
+        // UID_VPN: 6300 bytes, 0 packets
+        final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_with_clat);
+
+        assertValues(tunStats, CLAT_PREFIX + TEST_IFACE, UID_RED, 2000L, 200L, 1000, 100L);
+        assertValues(tunStats, CLAT_PREFIX + TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L);
+        assertValues(tunStats, CLAT_PREFIX + TEST_IFACE, UID_VPN, 6300L, 0L, 3150L, 0L);
+    }
+
+    @Test
+    public void testVpnWithOneUnderlyingIface() throws Exception {
+        final UnderlyingNetworkInfo[] underlyingNetworkInfos =
+                new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos);
+
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED
+        // over VPN.
+        // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE
+        // over VPN.
+        // VPN sent 1650 bytes (150 packets), and received 3300 (300 packets) over WiFi.
+        // Of 1650 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes
+        // attributed to UID_BLUE, and 150 bytes attributed to UID_VPN.
+        // Of 3300 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes
+        // attributed to UID_BLUE, and 300 bytes attributed to UID_VPN.
+        final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_one_underlying);
+
+        assertValues(tunStats, TEST_IFACE, UID_RED, 2000L, 200L, 1000L, 100L);
+        assertValues(tunStats, TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L);
+        assertValues(tunStats, TEST_IFACE, UID_VPN, 300L, 0L, 150L, 0L);
+    }
+
+    @Test
+    public void testVpnWithOneUnderlyingIfaceAndOwnTraffic() throws Exception {
+        // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE).
+        final UnderlyingNetworkInfo[] underlyingNetworkInfos =
+                new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos);
+
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED
+        // over VPN.
+        // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE
+        // over VPN.
+        // Additionally, the VPN sends 6000 bytes (600 packets) of its own traffic into the tun
+        // interface (passing that traffic to the VPN endpoint), and receives 5000 bytes (500
+        // packets) from it. Including overhead that is 6600/5500 bytes.
+        // VPN sent 8250 bytes (750 packets), and received 8800 (800 packets) over WiFi.
+        // Of 8250 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes
+        // attributed to UID_BLUE, and 6750 bytes attributed to UID_VPN.
+        // Of 8800 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes
+        // attributed to UID_BLUE, and 5800 bytes attributed to UID_VPN.
+        final NetworkStats tunStats =
+                parseDetailedStats(R.raw.xt_qtaguid_vpn_one_underlying_own_traffic);
+
+        assertValues(tunStats, TEST_IFACE, UID_RED, 2000L, 200L, 1000L, 100L);
+        assertValues(tunStats, TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L);
+        assertValues(tunStats, TEST_IFACE, UID_VPN, 5800L, 500L, 6750L, 600L);
+    }
+
+    @Test
+    public void testVpnWithOneUnderlyingIface_withCompression() throws Exception {
+        // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE).
+        final UnderlyingNetworkInfo[] underlyingNetworkInfos =
+                new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos);
+
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent/received by UID_RED over VPN.
+        // 3000 bytes (300 packets) were sent/received by UID_BLUE over VPN.
+        // VPN sent/received 1000 bytes (100 packets) over WiFi.
+        // Of 1000 bytes over WiFi, expect 250 bytes attributed UID_RED and 750 bytes to UID_BLUE,
+        // with nothing attributed to UID_VPN for both rx/tx traffic.
+        final NetworkStats tunStats =
+                parseDetailedStats(R.raw.xt_qtaguid_vpn_one_underlying_compression);
+
+        assertValues(tunStats, TEST_IFACE, UID_RED, 250L, 25L, 250L, 25L);
+        assertValues(tunStats, TEST_IFACE, UID_BLUE, 750L, 75L, 750L, 75L);
+        assertValues(tunStats, TEST_IFACE, UID_VPN, 0L, 0L, 0L, 0L);
+    }
+
+    @Test
+    public void testVpnWithTwoUnderlyingIfaces_packetDuplication() throws Exception {
+        // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and
+        // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set.
+        // Additionally, VPN is duplicating traffic across both WiFi and Cell.
+        final UnderlyingNetworkInfo[] underlyingNetworkInfos =
+                new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})};
+        mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos);
+
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent/received by UID_RED and UID_BLUE over VPN.
+        // VPN sent/received 4400 bytes (400 packets) over both WiFi and Cell (8800 bytes in total).
+        // Of 8800 bytes over WiFi/Cell, expect:
+        // - 500 bytes rx/tx each over WiFi/Cell attributed to both UID_RED and UID_BLUE.
+        // - 1200 bytes rx/tx each over WiFi/Cell for VPN_UID.
+        final NetworkStats tunStats =
+                parseDetailedStats(R.raw.xt_qtaguid_vpn_two_underlying_duplication);
+
+        assertValues(tunStats, TEST_IFACE, UID_RED, 500L, 50L, 500L, 50L);
+        assertValues(tunStats, TEST_IFACE, UID_BLUE, 500L, 50L, 500L, 50L);
+        assertValues(tunStats, TEST_IFACE, UID_VPN, 1200L, 100L, 1200L, 100L);
+        assertValues(tunStats, TEST_IFACE2, UID_RED, 500L, 50L, 500L, 50L);
+        assertValues(tunStats, TEST_IFACE2, UID_BLUE, 500L, 50L, 500L, 50L);
+        assertValues(tunStats, TEST_IFACE2, UID_VPN, 1200L, 100L, 1200L, 100L);
+    }
+
+    @Test
+    public void testConcurrentVpns() throws Exception {
+        // Assume two VPNs are connected on two different network interfaces. VPN1 is using
+        // TEST_IFACE and VPN2 is using TEST_IFACE2.
+        final UnderlyingNetworkInfo[] underlyingNetworkInfos = new UnderlyingNetworkInfo[] {
+                createVpnInfo(TUN_IFACE, new String[] {TEST_IFACE}),
+                createVpnInfo(TUN_IFACE2, new String[] {TEST_IFACE2})};
+        mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos);
+
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED
+        // over VPN1.
+        // 700 bytes (70 packets) were sent, and 3000 bytes (300 packets) were received by UID_RED
+        // over VPN2.
+        // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE
+        // over VPN1.
+        // 250 bytes (25 packets) were sent, and 500 bytes (50 packets) were received by UID_BLUE
+        // over VPN2.
+        // VPN1 sent 1650 bytes (150 packets), and received 3300 (300 packets) over TEST_IFACE.
+        // Of 1650 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes
+        // attributed to UID_BLUE, and 150 bytes attributed to UID_VPN.
+        // Of 3300 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes
+        // attributed to UID_BLUE, and 300 bytes attributed to UID_VPN.
+        // VPN2 sent 1045 bytes (95 packets), and received 3850 (350 packets) over TEST_IFACE2.
+        // Of 1045 bytes sent over Cell, expect 700 bytes attributed to UID_RED, 250 bytes
+        // attributed to UID_BLUE, and 95 bytes attributed to UID_VPN.
+        // Of 3850 bytes received over Cell, expect 3000 bytes attributed to UID_RED, 500 bytes
+        // attributed to UID_BLUE, and 350 bytes attributed to UID_VPN.
+        final NetworkStats tunStats =
+                parseDetailedStats(R.raw.xt_qtaguid_vpn_one_underlying_two_vpn);
+
+        assertValues(tunStats, TEST_IFACE, UID_RED, 2000L, 200L, 1000L, 100L);
+        assertValues(tunStats, TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L);
+        assertValues(tunStats, TEST_IFACE2, UID_RED, 3000L, 300L, 700L, 70L);
+        assertValues(tunStats, TEST_IFACE2, UID_BLUE, 500L, 50L, 250L, 25L);
+        assertValues(tunStats, TEST_IFACE, UID_VPN, 300L, 0L, 150L, 0L);
+        assertValues(tunStats, TEST_IFACE2, UID_VPN, 350L, 0L, 95L, 0L);
+    }
+
+    @Test
+    public void testVpnWithTwoUnderlyingIfaces_splitTraffic() throws Exception {
+        // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and
+        // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set.
+        // Additionally, VPN is arbitrarily splitting traffic across WiFi and Cell.
+        final UnderlyingNetworkInfo[] underlyingNetworkInfos =
+                new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})};
+        mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos);
+
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent, and 500 bytes (50 packets) received by UID_RED over
+        // VPN.
+        // VPN sent 660 bytes (60 packets) over WiFi and 440 bytes (40 packets) over Cell.
+        // And, it received 330 bytes (30 packets) over WiFi and 220 bytes (20 packets) over Cell.
+        // For UID_RED, expect 600 bytes attributed over WiFi and 400 bytes over Cell for sent (tx)
+        // traffic. For received (rx) traffic, expect 300 bytes over WiFi and 200 bytes over Cell.
+        //
+        // For UID_VPN, expect 60 bytes attributed over WiFi and 40 bytes over Cell for tx traffic.
+        // And, 30 bytes over WiFi and 20 bytes over Cell for rx traffic.
+        final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_two_underlying_split);
+
+        assertValues(tunStats, TEST_IFACE, UID_RED, 300L, 30L, 600L, 60L);
+        assertValues(tunStats, TEST_IFACE, UID_VPN, 30L, 0L, 60L, 0L);
+        assertValues(tunStats, TEST_IFACE2, UID_RED, 200L, 20L, 400L, 40L);
+        assertValues(tunStats, TEST_IFACE2, UID_VPN, 20L, 0L, 40L, 0L);
+    }
+
+    @Test
+    public void testVpnWithTwoUnderlyingIfaces_splitTrafficWithCompression() throws Exception {
+        // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and
+        // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set.
+        // Additionally, VPN is arbitrarily splitting compressed traffic across WiFi and Cell.
+        final UnderlyingNetworkInfo[] underlyingNetworkInfos =
+                new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})};
+        mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos);
+
+        // create some traffic (assume 10 bytes of MTU for VPN interface:
+        // 1000 bytes (100 packets) were sent/received by UID_RED over VPN.
+        // VPN sent/received 600 bytes (60 packets) over WiFi and 200 bytes (20 packets) over Cell.
+        // For UID_RED, expect 600 bytes attributed over WiFi and 200 bytes over Cell for both
+        // rx/tx.
+        // UID_VPN gets nothing attributed to it (avoiding negative stats).
+        final NetworkStats tunStats =
+                parseDetailedStats(R.raw.xt_qtaguid_vpn_two_underlying_split_compression);
+
+        assertValues(tunStats, TEST_IFACE, UID_RED, 600L, 60L, 600L, 60L);
+        assertValues(tunStats, TEST_IFACE, UID_VPN, 0L, 0L, 0L, 0L);
+        assertValues(tunStats, TEST_IFACE2, UID_RED, 200L, 20L, 200L, 20L);
+        assertValues(tunStats, TEST_IFACE2, UID_VPN, 0L, 0L, 0L, 0L);
+    }
+
+    @Test
+    public void testVpnWithIncorrectUnderlyingIface() throws Exception {
+        // WiFi and Cell networks are connected and VPN is using Cell (which has TEST_IFACE2),
+        // but has declared only WiFi (TEST_IFACE) in its underlying network set.
+        final UnderlyingNetworkInfo[] underlyingNetworkInfos =
+                new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos);
+
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent/received by UID_RED over VPN.
+        // VPN sent/received 1100 bytes (100 packets) over Cell.
+        // Of 1100 bytes over Cell, expect all of it attributed to UID_VPN for both rx/tx traffic.
+        final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_incorrect_iface);
+
+        assertValues(tunStats, TEST_IFACE, UID_RED, 0L, 0L, 0L, 0L);
+        assertValues(tunStats, TEST_IFACE, UID_VPN, 0L, 0L, 0L, 0L);
+        assertValues(tunStats, TEST_IFACE2, UID_RED, 0L, 0L, 0L, 0L);
+        assertValues(tunStats, TEST_IFACE2, UID_VPN, 1100L, 100L, 1100L, 100L);
+    }
+
+    @Test
+    public void testKernelTags() throws Exception {
+        assertEquals(0, kernelToTag("0x0000000000000000"));
+        assertEquals(0x32, kernelToTag("0x0000003200000000"));
+        assertEquals(2147483647, kernelToTag("0x7fffffff00000000"));
+        assertEquals(0, kernelToTag("0x0000000000000000"));
+        assertEquals(2147483136, kernelToTag("0x7FFFFE0000000000"));
+
+        assertEquals(0, kernelToTag("0x0"));
+        assertEquals(0, kernelToTag("0xf00d"));
+        assertEquals(1, kernelToTag("0x100000000"));
+        assertEquals(14438007, kernelToTag("0xdc4e7700000000"));
+        assertEquals(TrafficStats.TAG_SYSTEM_DOWNLOAD, kernelToTag("0xffffff0100000000"));
+    }
+
+    @Test
+    public void testNetworkStatsWithSet() throws Exception {
+        final NetworkStats stats = parseDetailedStats(R.raw.xt_qtaguid_typical);
+        assertEquals(70, stats.size());
+        assertStatsEntry(stats, "rmnet1", 10021, SET_DEFAULT, 0x30100000, 219110L, 578L, 227423L,
+                676L);
+        assertStatsEntry(stats, "rmnet1", 10021, SET_FOREGROUND, 0x30100000, 742L, 3L, 1265L, 3L);
+    }
+
+    @Test
+    public void testNetworkStatsSingle() throws Exception {
+        stageFile(R.raw.xt_qtaguid_iface_typical, file("net/xt_qtaguid/iface_stat_all"));
+
+        final NetworkStats stats = mFactory.readNetworkStatsSummaryDev();
+        assertEquals(6, stats.size());
+        assertStatsEntry(stats, "rmnet0", UID_ALL, SET_ALL, TAG_NONE, 2112L, 24L, 700L, 10L);
+        assertStatsEntry(stats, "test1", UID_ALL, SET_ALL, TAG_NONE, 6L, 8L, 10L, 12L);
+        assertStatsEntry(stats, "test2", UID_ALL, SET_ALL, TAG_NONE, 1L, 2L, 3L, 4L);
+    }
+
+    @Test
+    public void testNetworkStatsXt() throws Exception {
+        stageFile(R.raw.xt_qtaguid_iface_fmt_typical, file("net/xt_qtaguid/iface_stat_fmt"));
+
+        final NetworkStats stats = mFactory.readNetworkStatsSummaryXt();
+        assertEquals(3, stats.size());
+        assertStatsEntry(stats, "rmnet0", UID_ALL, SET_ALL, TAG_NONE, 6824L, 16L, 5692L, 10L);
+        assertStatsEntry(stats, "rmnet1", UID_ALL, SET_ALL, TAG_NONE, 11153922L, 8051L, 190226L,
+                2468L);
+        assertStatsEntry(stats, "rmnet2", UID_ALL, SET_ALL, TAG_NONE, 4968L, 35L, 3081L, 39L);
+    }
+
+    @Test
+    public void testDoubleClatAccountingSimple() throws Exception {
+        mFactory.noteStackedIface("v4-wlan0", "wlan0");
+
+        // xt_qtaguid_with_clat_simple is a synthetic file that simulates
+        //  - 213 received 464xlat packets of size 200 bytes
+        //  - 41 sent 464xlat packets of size 100 bytes
+        //  - no other traffic on base interface for root uid.
+        NetworkStats stats = parseDetailedStats(R.raw.xt_qtaguid_with_clat_simple);
+        assertEquals(3, stats.size());
+
+        assertStatsEntry(stats, "v4-wlan0", 10060, SET_DEFAULT, 0x0, 46860L, 4920L);
+        assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, 0L, 0L);
+    }
+
+    @Test
+    public void testDoubleClatAccounting() throws Exception {
+        mFactory.noteStackedIface("v4-wlan0", "wlan0");
+
+        NetworkStats stats = parseDetailedStats(R.raw.xt_qtaguid_with_clat);
+        assertEquals(42, stats.size());
+
+        assertStatsEntry(stats, "v4-wlan0", 0, SET_DEFAULT, 0x0, 356L, 276L);
+        assertStatsEntry(stats, "v4-wlan0", 1000, SET_DEFAULT, 0x0, 30812L, 2310L);
+        assertStatsEntry(stats, "v4-wlan0", 10102, SET_DEFAULT, 0x0, 10022L, 3330L);
+        assertStatsEntry(stats, "v4-wlan0", 10060, SET_DEFAULT, 0x0, 9532772L, 254112L);
+        assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, 0L, 0L);
+        assertStatsEntry(stats, "wlan0", 1000, SET_DEFAULT, 0x0, 6126L, 2013L);
+        assertStatsEntry(stats, "wlan0", 10013, SET_DEFAULT, 0x0, 0L, 144L);
+        assertStatsEntry(stats, "wlan0", 10018, SET_DEFAULT, 0x0, 5980263L, 167667L);
+        assertStatsEntry(stats, "wlan0", 10060, SET_DEFAULT, 0x0, 134356L, 8705L);
+        assertStatsEntry(stats, "wlan0", 10079, SET_DEFAULT, 0x0, 10926L, 1507L);
+        assertStatsEntry(stats, "wlan0", 10102, SET_DEFAULT, 0x0, 25038L, 8245L);
+        assertStatsEntry(stats, "wlan0", 10103, SET_DEFAULT, 0x0, 0L, 192L);
+        assertStatsEntry(stats, "dummy0", 0, SET_DEFAULT, 0x0, 0L, 168L);
+        assertStatsEntry(stats, "lo", 0, SET_DEFAULT, 0x0, 1288L, 1288L);
+
+        assertNoStatsEntry(stats, "wlan0", 1029, SET_DEFAULT, 0x0);
+    }
+
+    @Test
+    public void testDoubleClatAccounting100MBDownload() throws Exception {
+        // Downloading 100mb from an ipv4 only destination in a foreground activity
+
+        long appRxBytesBefore = 328684029L;
+        long appRxBytesAfter = 439237478L;
+        assertEquals("App traffic should be ~100MB", 110553449, appRxBytesAfter - appRxBytesBefore);
+
+        long rootRxBytes = 330187296L;
+
+        mFactory.noteStackedIface("v4-wlan0", "wlan0");
+        NetworkStats stats;
+
+        // Stats snapshot before the download
+        stats = parseDetailedStats(R.raw.xt_qtaguid_with_clat_100mb_download_before);
+        assertStatsEntry(stats, "v4-wlan0", 10106, SET_FOREGROUND, 0x0, appRxBytesBefore, 5199872L);
+        assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, rootRxBytes, 0L);
+
+        // Stats snapshot after the download
+        stats = parseDetailedStats(R.raw.xt_qtaguid_with_clat_100mb_download_after);
+        assertStatsEntry(stats, "v4-wlan0", 10106, SET_FOREGROUND, 0x0, appRxBytesAfter, 7867488L);
+        assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, rootRxBytes, 0L);
+    }
+
+    /**
+     * Copy a {@link Resources#openRawResource(int)} into {@link File} for
+     * testing purposes.
+     */
+    private void stageFile(int rawId, File file) throws Exception {
+        new File(file.getParent()).mkdirs();
+        InputStream in = null;
+        OutputStream out = null;
+        try {
+            in = InstrumentationRegistry.getContext().getResources().openRawResource(rawId);
+            out = new FileOutputStream(file);
+            Streams.copy(in, out);
+        } finally {
+            IoUtils.closeQuietly(in);
+            IoUtils.closeQuietly(out);
+        }
+    }
+
+    private void stageLong(long value, File file) throws Exception {
+        new File(file.getParent()).mkdirs();
+        FileWriter out = null;
+        try {
+            out = new FileWriter(file);
+            out.write(Long.toString(value));
+        } finally {
+            IoUtils.closeQuietly(out);
+        }
+    }
+
+    private File file(String path) throws Exception {
+        return new File(mTestProc, path);
+    }
+
+    private NetworkStats parseDetailedStats(int resourceId) throws Exception {
+        stageFile(resourceId, file("net/xt_qtaguid/stats"));
+        return mFactory.readNetworkStatsDetail();
+    }
+
+    private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set,
+            int tag, long rxBytes, long txBytes) {
+        final int i = stats.findIndex(iface, uid, set, tag, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO);
+        if (i < 0) {
+            fail(String.format("no NetworkStats for (iface: %s, uid: %d, set: %d, tag: %d)",
+                    iface, uid, set, tag));
+        }
+        final NetworkStats.Entry entry = stats.getValues(i, null);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+    }
+
+    private static void assertNoStatsEntry(NetworkStats stats, String iface, int uid, int set,
+            int tag) {
+        final int i = stats.findIndex(iface, uid, set, tag, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO);
+        if (i >= 0) {
+            fail("unexpected NetworkStats entry at " + i);
+        }
+    }
+
+    private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set,
+            int tag, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+        assertStatsEntry(stats, iface, uid, set, tag, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO,
+                rxBytes, rxPackets, txBytes, txPackets);
+    }
+
+    private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set,
+            int tag, int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets,
+            long txBytes, long txPackets) {
+        final int i = stats.findIndex(iface, uid, set, tag, metered, roaming, defaultNetwork);
+
+        if (i < 0) {
+            fail(String.format("no NetworkStats for (iface: %s, uid: %d, set: %d, tag: %d, metered:"
+                    + " %d, roaming: %d, defaultNetwork: %d)",
+                    iface, uid, set, tag, metered, roaming, defaultNetwork));
+        }
+        final NetworkStats.Entry entry = stats.getValues(i, null);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+        assertEquals("unexpected txPackets", txPackets, entry.txPackets);
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
new file mode 100644
index 0000000..9fa1c50
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -0,0 +1,447 @@
+/*
+ * 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.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.NetworkIdentity.OEM_NONE;
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.ROAMING_NO;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkTemplate.buildTemplateMobileAll;
+import static android.net.NetworkTemplate.buildTemplateWifiWildcard;
+import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+
+import android.app.usage.NetworkStatsManager;
+import android.net.DataUsageRequest;
+import android.net.NetworkIdentity;
+import android.net.NetworkStats;
+import android.net.NetworkTemplate;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Messenger;
+import android.os.Process;
+import android.os.UserHandle;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.net.NetworkStatsServiceTest.LatchedHandler;
+import com.android.testutils.HandlerUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Objects;
+
+/**
+ * Tests for {@link NetworkStatsObservers}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkStatsObserversTest {
+    private static final String TEST_IFACE = "test0";
+    private static final String TEST_IFACE2 = "test1";
+    private static final long TEST_START = 1194220800000L;
+
+    private static final String IMSI_1 = "310004";
+    private static final String IMSI_2 = "310260";
+    private static final String TEST_SSID = "AndroidAP";
+
+    private static NetworkTemplate sTemplateWifi = buildTemplateWifiWildcard();
+    private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1);
+    private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2);
+
+    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 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;
+    private static final int INVALID_TYPE = -1;
+
+    private long mElapsedRealtime;
+
+    private HandlerThread mObserverHandlerThread;
+    private Handler mObserverNoopHandler;
+
+    private LatchedHandler mHandler;
+
+    private NetworkStatsObservers mStatsObservers;
+    private Messenger mMessenger;
+    private ArrayMap<String, NetworkIdentitySet> mActiveIfaces;
+    private ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces;
+
+    @Mock private IBinder mockBinder;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mObserverHandlerThread = new HandlerThread("HandlerThread");
+        mObserverHandlerThread.start();
+        final Looper observerLooper = mObserverHandlerThread.getLooper();
+        mStatsObservers = new NetworkStatsObservers() {
+            @Override
+            protected Looper getHandlerLooperLocked() {
+                return observerLooper;
+            }
+        };
+
+        mHandler = new LatchedHandler(Looper.getMainLooper(), new ConditionVariable());
+        mMessenger = new Messenger(mHandler);
+
+        mActiveIfaces = new ArrayMap<>();
+        mActiveUidIfaces = new ArrayMap<>();
+    }
+
+    @Test
+    public void testRegister_thresholdTooLow_setsDefaultThreshold() throws Exception {
+        long thresholdTooLowBytes = 1L;
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdTooLowBytes);
+
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(request.requestId > 0);
+        assertTrue(Objects.equals(sTemplateWifi, request.template));
+        assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+    }
+
+    @Test
+    public void testRegister_highThreshold_accepted() throws Exception {
+        long highThresholdBytes = 2 * THRESHOLD_BYTES;
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, highThresholdBytes);
+
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(request.requestId > 0);
+        assertTrue(Objects.equals(sTemplateWifi, request.template));
+        assertEquals(highThresholdBytes, request.thresholdInBytes);
+    }
+
+    @Test
+    public void testRegister_twoRequests_twoIds() throws Exception {
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
+
+        DataUsageRequest request1 = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(request1.requestId > 0);
+        assertTrue(Objects.equals(sTemplateWifi, request1.template));
+        assertEquals(THRESHOLD_BYTES, request1.thresholdInBytes);
+
+        DataUsageRequest request2 = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(request2.requestId > request1.requestId);
+        assertTrue(Objects.equals(sTemplateWifi, request2.template));
+        assertEquals(THRESHOLD_BYTES, request2.thresholdInBytes);
+    }
+
+    @Test
+    public void testUnregister_unknownRequest_noop() throws Exception {
+        DataUsageRequest unknownRequest = new DataUsageRequest(
+                123456 /* id */, sTemplateWifi, THRESHOLD_BYTES);
+
+        mStatsObservers.unregister(unknownRequest, UID_RED);
+    }
+
+    @Test
+    public void testUnregister_knownRequest_releasesCaller() throws Exception {
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
+
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(request.requestId > 0);
+        assertTrue(Objects.equals(sTemplateImsi1, request.template));
+        assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+        Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+
+        mStatsObservers.unregister(request, Process.SYSTEM_UID);
+        waitForObserverToIdle();
+
+        Mockito.verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+    }
+
+    @Test
+    public void testUnregister_knownRequest_invalidUid_doesNotUnregister() throws Exception {
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
+
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                UID_RED, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(request.requestId > 0);
+        assertTrue(Objects.equals(sTemplateImsi1, request.template));
+        assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+        Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+
+        mStatsObservers.unregister(request, UID_BLUE);
+        waitForObserverToIdle();
+
+        Mockito.verifyZeroInteractions(mockBinder);
+    }
+
+    private NetworkIdentitySet makeTestIdentSet() {
+        NetworkIdentitySet identSet = new NetworkIdentitySet();
+        identSet.add(new NetworkIdentity(
+                TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN,
+                IMSI_1, null /* networkId */, false /* roaming */, true /* metered */,
+                true /* defaultNetwork */, OEM_NONE));
+        return identSet;
+    }
+
+    @Test
+    public void testUpdateStats_initialSample_doesNotNotify() throws Exception {
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
+
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(request.requestId > 0);
+        assertTrue(Objects.equals(sTemplateImsi1, request.template));
+        assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+
+        NetworkIdentitySet identSet = makeTestIdentSet();
+        mActiveIfaces.put(TEST_IFACE, identSet);
+
+        // Baseline
+        NetworkStats xtSnapshot = new NetworkStats(TEST_START, 1 /* initialSize */)
+                .insertEntry(TEST_IFACE, BASE_BYTES, 8L, BASE_BYTES, 16L);
+        NetworkStats uidSnapshot = null;
+
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+        waitForObserverToIdle();
+    }
+
+    @Test
+    public void testUpdateStats_belowThreshold_doesNotNotify() throws Exception {
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
+
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(request.requestId > 0);
+        assertTrue(Objects.equals(sTemplateImsi1, request.template));
+        assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+
+        NetworkIdentitySet identSet = makeTestIdentSet();
+        mActiveIfaces.put(TEST_IFACE, identSet);
+
+        // Baseline
+        NetworkStats xtSnapshot = new NetworkStats(TEST_START, 1 /* initialSize */)
+                .insertEntry(TEST_IFACE, BASE_BYTES, 8L, BASE_BYTES, 16L);
+        NetworkStats uidSnapshot = null;
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+
+        // Delta
+        xtSnapshot = new NetworkStats(TEST_START, 1 /* initialSize */)
+                .insertEntry(TEST_IFACE, BASE_BYTES + 1024L, 10L, BASE_BYTES + 2048L, 20L);
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+        waitForObserverToIdle();
+    }
+
+
+    @Test
+    public void testUpdateStats_deviceAccess_notifies() throws Exception {
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
+
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+        assertTrue(request.requestId > 0);
+        assertTrue(Objects.equals(sTemplateImsi1, request.template));
+        assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+
+        NetworkIdentitySet identSet = makeTestIdentSet();
+        mActiveIfaces.put(TEST_IFACE, identSet);
+
+        // Baseline
+        NetworkStats xtSnapshot = new NetworkStats(TEST_START, 1 /* initialSize */)
+                .insertEntry(TEST_IFACE, BASE_BYTES, 8L, BASE_BYTES, 16L);
+        NetworkStats uidSnapshot = null;
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+
+        // Delta
+        xtSnapshot = new NetworkStats(TEST_START + MINUTE_IN_MILLIS, 1 /* initialSize */)
+                .insertEntry(TEST_IFACE, BASE_BYTES + THRESHOLD_BYTES, 12L,
+                        BASE_BYTES + THRESHOLD_BYTES, 22L);
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+        waitForObserverToIdle();
+        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
+    }
+
+    @Test
+    public void testUpdateStats_defaultAccess_notifiesSameUid() throws Exception {
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
+
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                UID_RED, NetworkStatsAccess.Level.DEFAULT);
+        assertTrue(request.requestId > 0);
+        assertTrue(Objects.equals(sTemplateImsi1, request.template));
+        assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+
+        NetworkIdentitySet identSet = makeTestIdentSet();
+        mActiveUidIfaces.put(TEST_IFACE, identSet);
+
+        // Baseline
+        NetworkStats xtSnapshot = null;
+        NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L);
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+
+        // Delta
+        uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, BASE_BYTES + THRESHOLD_BYTES, 2L,
+                        BASE_BYTES + THRESHOLD_BYTES, 2L, 0L);
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+        waitForObserverToIdle();
+        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
+    }
+
+    @Test
+    public void testUpdateStats_defaultAccess_usageOtherUid_doesNotNotify() throws Exception {
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
+
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                UID_BLUE, NetworkStatsAccess.Level.DEFAULT);
+        assertTrue(request.requestId > 0);
+        assertTrue(Objects.equals(sTemplateImsi1, request.template));
+        assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+
+        NetworkIdentitySet identSet = makeTestIdentSet();
+        mActiveUidIfaces.put(TEST_IFACE, identSet);
+
+        // Baseline
+        NetworkStats xtSnapshot = null;
+        NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L);
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+
+        // Delta
+        uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, BASE_BYTES + THRESHOLD_BYTES, 2L,
+                        BASE_BYTES + THRESHOLD_BYTES, 2L, 0L);
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+        waitForObserverToIdle();
+    }
+
+    @Test
+    public void testUpdateStats_userAccess_usageSameUser_notifies() throws Exception {
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
+
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                UID_BLUE, NetworkStatsAccess.Level.USER);
+        assertTrue(request.requestId > 0);
+        assertTrue(Objects.equals(sTemplateImsi1, request.template));
+        assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+
+        NetworkIdentitySet identSet = makeTestIdentSet();
+        mActiveUidIfaces.put(TEST_IFACE, identSet);
+
+        // Baseline
+        NetworkStats xtSnapshot = null;
+        NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L);
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+
+        // Delta
+        uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, BASE_BYTES + THRESHOLD_BYTES, 2L,
+                        BASE_BYTES + THRESHOLD_BYTES, 2L, 0L);
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+        waitForObserverToIdle();
+        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
+    }
+
+    @Test
+    public void testUpdateStats_userAccess_usageAnotherUser_doesNotNotify() throws Exception {
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
+
+        DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+                UID_RED, NetworkStatsAccess.Level.USER);
+        assertTrue(request.requestId > 0);
+        assertTrue(Objects.equals(sTemplateImsi1, request.template));
+        assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+
+        NetworkIdentitySet identSet = makeTestIdentSet();
+        mActiveUidIfaces.put(TEST_IFACE, identSet);
+
+        // Baseline
+        NetworkStats xtSnapshot = null;
+        NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */)
+                .insertEntry(TEST_IFACE, UID_ANOTHER_USER, SET_DEFAULT, TAG_NONE, METERED_NO,
+                        ROAMING_NO, DEFAULT_NETWORK_YES, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L);
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+
+        // Delta
+        uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */)
+                .insertEntry(TEST_IFACE, UID_ANOTHER_USER, SET_DEFAULT, TAG_NONE, METERED_NO,
+                        ROAMING_NO, DEFAULT_NETWORK_NO, BASE_BYTES + THRESHOLD_BYTES, 2L,
+                        BASE_BYTES + THRESHOLD_BYTES, 2L, 0L);
+        mStatsObservers.updateStats(
+                xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
+        waitForObserverToIdle();
+    }
+
+    private void waitForObserverToIdle() {
+        HandlerUtils.waitForIdle(mObserverHandlerThread, WAIT_TIMEOUT_MS);
+        HandlerUtils.waitForIdle(mHandler, WAIT_TIMEOUT_MS);
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
new file mode 100644
index 0000000..ee94ae9
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -0,0 +1,1833 @@
+/*
+ * 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.READ_NETWORK_USAGE_HISTORY;
+import static android.Manifest.permission.UPDATE_DEVICE_STATS;
+import static android.content.Intent.ACTION_UID_REMOVED;
+import static android.content.Intent.EXTRA_UID;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.NetworkIdentity.OEM_PAID;
+import static android.net.NetworkIdentity.OEM_PRIVATE;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+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.IFACE_ALL;
+import static android.net.NetworkStats.INTERFACES_ALL;
+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 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_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.MATCH_MOBILE_WILDCARD;
+import static android.net.NetworkTemplate.NETWORK_TYPE_ALL;
+import static android.net.NetworkTemplate.OEM_MANAGED_NO;
+import static android.net.NetworkTemplate.OEM_MANAGED_YES;
+import static android.net.NetworkTemplate.SUBSCRIBER_ID_MATCH_RULE_EXACT;
+import static android.net.NetworkTemplate.buildTemplateMobileAll;
+import static android.net.NetworkTemplate.buildTemplateMobileWithRatType;
+import static android.net.NetworkTemplate.buildTemplateWifi;
+import static android.net.NetworkTemplate.buildTemplateWifiWildcard;
+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.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.WEEK_IN_MILLIS;
+
+import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.AlarmManager;
+import android.app.usage.NetworkStatsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.net.DataUsageRequest;
+import android.net.INetworkManagementEventObserver;
+import android.net.INetworkStatsSession;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkStateSnapshot;
+import android.net.NetworkStats;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.UnderlyingNetworkInfo;
+import android.net.netstats.provider.INetworkStatsProviderCallback;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.PowerManager;
+import android.os.SimpleClock;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+
+import androidx.annotation.Nullable;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
+import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config;
+import com.android.testutils.HandlerUtils;
+import com.android.testutils.TestableNetworkStatsProviderBinder;
+
+import libcore.testing.io.TestIoUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+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;
+
+/**
+ * Tests for {@link NetworkStatsService}.
+ *
+ * TODO: This test used to be really brittle because it used Easymock - it uses Mockito now, but
+ * still uses the Easymock structure, which could be simplified.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
+    private static final String TAG = "NetworkStatsServiceTest";
+
+    private static final long TEST_START = 1194220800000L;
+
+    private static final String IMSI_1 = "310004";
+    private static final String IMSI_2 = "310260";
+    private static final String TEST_SSID = "AndroidAP";
+
+    private static NetworkTemplate sTemplateWifi = buildTemplateWifi(TEST_SSID);
+    private static NetworkTemplate sTemplateCarrierWifi1 =
+            buildTemplateWifi(NetworkTemplate.WIFI_NETWORKID_ALL, IMSI_1);
+    private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1);
+    private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2);
+
+    private static final Network WIFI_NETWORK =  new Network(100);
+    private static final Network MOBILE_NETWORK =  new Network(101);
+    private static final Network VPN_NETWORK = new Network(102);
+
+    private static final Network[] NETWORKS_WIFI = new Network[]{ WIFI_NETWORK };
+    private static final Network[] NETWORKS_MOBILE = new Network[]{ MOBILE_NETWORK };
+
+    private static final long WAIT_TIMEOUT = 2 * 1000;  // 2 secs
+    private static final int INVALID_TYPE = -1;
+
+    private long mElapsedRealtime;
+
+    private File mStatsDir;
+    private MockContext mServiceContext;
+    private @Mock TelephonyManager mTelephonyManager;
+    private @Mock INetworkManagementService mNetManager;
+    private @Mock NetworkStatsFactory mStatsFactory;
+    private @Mock NetworkStatsSettings mSettings;
+    private @Mock IBinder mBinder;
+    private @Mock AlarmManager mAlarmManager;
+    @Mock
+    private NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor;
+    private HandlerThread mHandlerThread;
+
+    private NetworkStatsService mService;
+    private INetworkStatsSession mSession;
+    private INetworkManagementEventObserver mNetworkObserver;
+    private ContentObserver mContentObserver;
+    private Handler mHandler;
+
+    private class MockContext extends BroadcastInterceptingContext {
+        private final Context mBaseContext;
+
+        MockContext(Context base) {
+            super(base);
+            mBaseContext = base;
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager;
+            return mBaseContext.getSystemService(name);
+        }
+
+        @Override
+        public void enforceCallingOrSelfPermission(String permission, @Nullable String message) {
+            if (checkCallingOrSelfPermission(permission) != PERMISSION_GRANTED) {
+                throw new SecurityException("Test does not have mocked permission " + permission);
+            }
+        }
+
+        @Override
+        public int checkCallingOrSelfPermission(String permission) {
+            switch (permission) {
+                case PERMISSION_MAINLINE_NETWORK_STACK:
+                case READ_NETWORK_USAGE_HISTORY:
+                case UPDATE_DEVICE_STATS:
+                    return PERMISSION_GRANTED;
+                default:
+                    return PERMISSION_DENIED;
+            }
+
+        }
+    }
+
+    private final Clock mClock = new SimpleClock(ZoneOffset.UTC) {
+        @Override
+        public long millis() {
+            return currentTimeMillis();
+        }
+    };
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        final Context context = InstrumentationRegistry.getContext();
+        mServiceContext = new MockContext(context);
+        mStatsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
+
+        PowerManager powerManager = (PowerManager) mServiceContext.getSystemService(
+                Context.POWER_SERVICE);
+        PowerManager.WakeLock wakeLock =
+                powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+
+        mHandlerThread = new HandlerThread("HandlerThread");
+        final NetworkStatsService.Dependencies deps = makeDependencies();
+        mService = new NetworkStatsService(mServiceContext, mNetManager, mAlarmManager, wakeLock,
+                mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), mStatsDir,
+                getBaseDir(mStatsDir), deps);
+
+        mElapsedRealtime = 0L;
+
+        expectDefaultSettings();
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+        mService.systemReady();
+        // Verify that system ready fetches realtime stats
+        verify(mStatsFactory).readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL);
+        // Wait for posting onChange() event to handler thread and verify that when system ready,
+        // start monitoring data usage per RAT type because the settings value is mock as false
+        // by default in expectSettings().
+        waitForIdle();
+        verify(mNetworkStatsSubscriptionsMonitor).start();
+        reset(mNetworkStatsSubscriptionsMonitor);
+
+        doReturn(TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS).when(mTelephonyManager)
+                .checkCarrierPrivilegesForPackageAnyPhone(anyString());
+
+        mSession = mService.openSession();
+        assertNotNull("openSession() failed", mSession);
+
+        // catch INetworkManagementEventObserver during systemReady()
+        ArgumentCaptor<INetworkManagementEventObserver> networkObserver =
+                ArgumentCaptor.forClass(INetworkManagementEventObserver.class);
+        verify(mNetManager).registerObserver(networkObserver.capture());
+        mNetworkObserver = networkObserver.getValue();
+    }
+
+    @NonNull
+    private NetworkStatsService.Dependencies makeDependencies() {
+        return new NetworkStatsService.Dependencies() {
+            @Override
+            public HandlerThread makeHandlerThread() {
+                return mHandlerThread;
+            }
+
+            @Override
+            public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(
+                    @NonNull Context context, @NonNull Looper looper, @NonNull Executor executor,
+                    @NonNull NetworkStatsService service) {
+
+                return mNetworkStatsSubscriptionsMonitor;
+            }
+
+            @Override
+            public ContentObserver makeContentObserver(Handler handler,
+                    NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) {
+                mHandler = handler;
+                return mContentObserver = super.makeContentObserver(handler, settings, monitor);
+            }
+
+        };
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mServiceContext = null;
+        mStatsDir = null;
+
+        mNetManager = null;
+        mSettings = null;
+
+        mSession.close();
+        mService = null;
+
+        mHandlerThread.quitSafely();
+    }
+
+    private void initWifiStats(NetworkStateSnapshot snapshot) throws Exception {
+        // pretend that wifi network comes online; service should ask about full
+        // network state, and poll any existing interfaces before updating.
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {snapshot};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+    }
+
+    private void incrementWifiStats(long durationMillis, String iface,
+            long rxb, long rxp, long txb, long txp) throws Exception {
+        incrementCurrentTime(durationMillis);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(iface, rxb, rxp, txb, txp));
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        forcePollAndWaitForIdle();
+    }
+
+    @Test
+    public void testNetworkStatsCarrierWifi() throws Exception {
+        initWifiStats(buildWifiState(true, TEST_IFACE, IMSI_1));
+        // verify service has empty history for carrier merged wifi and non-carrier wifi
+        assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
+        assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
+
+        // modify some number on wifi, and trigger poll event
+        incrementWifiStats(HOUR_IN_MILLIS, TEST_IFACE, 1024L, 1L, 2048L, 2L);
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateCarrierWifi1, 1024L, 1L, 2048L, 2L, 0);
+
+        // verify service recorded history for wifi with SSID filter
+        assertNetworkTotal(sTemplateWifi,  1024L, 1L, 2048L, 2L, 0);
+
+
+        // and bump forward again, with counters going higher. this is
+        // important, since polling should correctly subtract last snapshot.
+        incrementWifiStats(DAY_IN_MILLIS, TEST_IFACE, 4096L, 4L, 8192L, 8L);
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateCarrierWifi1, 4096L, 4L, 8192L, 8L, 0);
+        // verify service recorded history for wifi with SSID filter
+        assertNetworkTotal(sTemplateWifi, 4096L, 4L, 8192L, 8L, 0);
+    }
+
+    @Test
+    public void testNetworkStatsNonCarrierWifi() throws Exception {
+        initWifiStats(buildWifiState());
+
+        // verify service has empty history for wifi
+        assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
+        // verify service has empty history for carrier merged wifi
+        assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
+
+        // modify some number on wifi, and trigger poll event
+        incrementWifiStats(HOUR_IN_MILLIS, TEST_IFACE, 1024L, 1L, 2048L, 2L);
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0);
+        // verify service has empty history for carrier wifi since current network is non carrier
+        // wifi
+        assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
+
+        // and bump forward again, with counters going higher. this is
+        // important, since polling should correctly subtract last snapshot.
+        incrementWifiStats(DAY_IN_MILLIS, TEST_IFACE, 4096L, 4L, 8192L, 8L);
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateWifi, 4096L, 4L, 8192L, 8L, 0);
+        // verify service has empty history for carrier wifi since current network is non carrier
+        // wifi
+        assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
+    }
+
+    @Test
+    public void testStatsRebootPersist() throws Exception {
+        assertStatsFilesExist(false);
+
+        // pretend that wifi network comes online; service should ask about full
+        // network state, and poll any existing interfaces before updating.
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // verify service has empty history for wifi
+        assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 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.setUidForeground(UID_RED, false);
+        mService.incrementOperationCount(UID_RED, 0xFAAD, 4);
+        mService.setUidForeground(UID_RED, true);
+        mService.incrementOperationCount(UID_RED, 0xFAAD, 6);
+
+        forcePollAndWaitForIdle();
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0);
+        assertUidTotal(sTemplateWifi, UID_RED, 1024L, 8L, 512L, 4L, 10);
+        assertUidTotal(sTemplateWifi, UID_RED, SET_DEFAULT, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 512L, 4L, 256L, 2L, 4);
+        assertUidTotal(sTemplateWifi, UID_RED, SET_FOREGROUND, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 512L, 4L, 256L, 2L, 6);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 128L, 1L, 128L, 1L, 0);
+
+
+        // graceful shutdown system, which should trigger persist of stats, and
+        // clear any values in memory.
+        expectDefaultSettings();
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+        assertStatsFilesExist(true);
+
+        // boot through serviceReady() again
+        expectDefaultSettings();
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+
+        mService.systemReady();
+
+        // after systemReady(), we should have historical stats loaded again
+        assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0);
+        assertUidTotal(sTemplateWifi, UID_RED, 1024L, 8L, 512L, 4L, 10);
+        assertUidTotal(sTemplateWifi, UID_RED, SET_DEFAULT, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 512L, 4L, 256L, 2L, 4);
+        assertUidTotal(sTemplateWifi, UID_RED, SET_FOREGROUND, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 512L, 4L, 256L, 2L, 6);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 128L, 1L, 128L, 1L, 0);
+
+    }
+
+    // TODO: simulate reboot to test bucket resize
+    @Test
+    @Ignore
+    public void testStatsBucketResize() throws Exception {
+        NetworkStatsHistory history = null;
+
+        assertStatsFilesExist(false);
+
+        // pretend that wifi network comes online; service should ask about full
+        // network state, and poll any existing interfaces before updating.
+        expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // modify some number on wifi, and trigger poll event
+        incrementCurrentTime(2 * HOUR_IN_MILLIS);
+        expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 512L, 4L, 512L, 4L));
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        forcePollAndWaitForIdle();
+
+        // verify service recorded history
+        history = mSession.getHistoryForNetwork(sTemplateWifi, FIELD_ALL);
+        assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, 512L, 4L, 512L, 4L, 0);
+        assertEquals(HOUR_IN_MILLIS, history.getBucketDuration());
+        assertEquals(2, history.size());
+
+
+        // now change bucket duration setting and trigger another poll with
+        // exact same values, which should resize existing buckets.
+        expectSettings(0L, 30 * MINUTE_IN_MILLIS, WEEK_IN_MILLIS);
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        forcePollAndWaitForIdle();
+
+        // verify identical stats, but spread across 4 buckets now
+        history = mSession.getHistoryForNetwork(sTemplateWifi, FIELD_ALL);
+        assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, 512L, 4L, 512L, 4L, 0);
+        assertEquals(30 * MINUTE_IN_MILLIS, history.getBucketDuration());
+        assertEquals(4, history.size());
+
+    }
+
+    @Test
+    public void testUidStatsAcrossNetworks() throws Exception {
+        // pretend first mobile network comes online
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildMobile3gState(IMSI_1)};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // create some traffic on first network
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 2048L, 16L, 512L, 4L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L));
+        mService.incrementOperationCount(UID_RED, 0xF00D, 10);
+
+        forcePollAndWaitForIdle();
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0);
+        assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
+        assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 512L, 4L, 10);
+        assertUidTotal(sTemplateImsi1, UID_BLUE, 512L, 4L, 0L, 0L, 0);
+
+
+        // now switch networks; this also tests that we're okay with interfaces
+        // disappearing, to verify we don't count backwards.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        states = new NetworkStateSnapshot[] {buildMobile3gState(IMSI_2)};
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 2048L, 16L, 512L, 4L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L));
+
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+        forcePollAndWaitForIdle();
+
+
+        // create traffic on second network
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 2176L, 17L, 1536L, 12L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 640L, 5L, 1024L, 8L, 0L)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xFAAD, 128L, 1L, 1024L, 8L, 0L));
+        mService.incrementOperationCount(UID_BLUE, 0xFAAD, 10);
+
+        forcePollAndWaitForIdle();
+
+        // verify original history still intact
+        assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0);
+        assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 512L, 4L, 10);
+        assertUidTotal(sTemplateImsi1, UID_BLUE, 512L, 4L, 0L, 0L, 0);
+
+        // and verify new history also recorded under different template, which
+        // verifies that we didn't cross the streams.
+        assertNetworkTotal(sTemplateImsi2, 128L, 1L, 1024L, 8L, 0);
+        assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
+        assertUidTotal(sTemplateImsi2, UID_BLUE, 128L, 1L, 1024L, 8L, 10);
+
+    }
+
+    @Test
+    public void testUidRemovedIsMoved() throws Exception {
+        // pretend that network comes online
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // create some traffic
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 4128L, 258L, 544L, 34L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 16L, 1L, 16L, 1L, 0L)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE,
+                        4096L, 258L, 512L, 32L, 0L)
+                .insertEntry(TEST_IFACE, UID_GREEN, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L));
+        mService.incrementOperationCount(UID_RED, 0xFAAD, 10);
+
+        forcePollAndWaitForIdle();
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateWifi, 4128L, 258L, 544L, 34L, 0);
+        assertUidTotal(sTemplateWifi, UID_RED, 16L, 1L, 16L, 1L, 10);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 4096L, 258L, 512L, 32L, 0);
+        assertUidTotal(sTemplateWifi, UID_GREEN, 16L, 1L, 16L, 1L, 0);
+
+
+        // now pretend two UIDs are uninstalled, which should migrate stats to
+        // special "removed" bucket.
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 4128L, 258L, 544L, 34L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 16L, 1L, 16L, 1L, 0L)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE,
+                        4096L, 258L, 512L, 32L, 0L)
+                .insertEntry(TEST_IFACE, UID_GREEN, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L));
+        final Intent intent = new Intent(ACTION_UID_REMOVED);
+        intent.putExtra(EXTRA_UID, UID_BLUE);
+        mServiceContext.sendBroadcast(intent);
+        intent.putExtra(EXTRA_UID, UID_RED);
+        mServiceContext.sendBroadcast(intent);
+
+        // existing uid and total should remain unchanged; but removed UID
+        // should be gone completely.
+        assertNetworkTotal(sTemplateWifi, 4128L, 258L, 544L, 34L, 0);
+        assertUidTotal(sTemplateWifi, UID_RED, 0L, 0L, 0L, 0L, 0);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 0L, 0L, 0L, 0L, 0);
+        assertUidTotal(sTemplateWifi, UID_GREEN, 16L, 1L, 16L, 1L, 0);
+        assertUidTotal(sTemplateWifi, UID_REMOVED, 4112L, 259L, 528L, 33L, 10);
+
+    }
+
+    @Test
+    public void testMobileStatsByRatType() throws Exception {
+        final NetworkTemplate template3g =
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS);
+        final NetworkTemplate template4g =
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_LTE);
+        final NetworkTemplate template5g =
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_NR);
+        final NetworkStateSnapshot[] states =
+                new NetworkStateSnapshot[]{buildMobile3gState(IMSI_1)};
+
+        // 3G network comes online.
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS);
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Create some traffic.
+        incrementCurrentTime(MINUTE_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+                        12L, 18L, 14L, 1L, 0L)));
+        forcePollAndWaitForIdle();
+
+        // Verify 3g templates gets stats.
+        assertUidTotal(sTemplateImsi1, UID_RED, 12L, 18L, 14L, 1L, 0);
+        assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0);
+        assertUidTotal(template4g, UID_RED, 0L, 0L, 0L, 0L, 0);
+        assertUidTotal(template5g, UID_RED, 0L, 0L, 0L, 0L, 0);
+
+        // 4G network comes online.
+        incrementCurrentTime(MINUTE_IN_MILLIS);
+        setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_LTE);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                // Append more traffic on existing 3g stats entry.
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+                        16L, 22L, 17L, 2L, 0L))
+                // Add entry that is new on 4g.
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE,
+                        33L, 27L, 8L, 10L, 1L)));
+        forcePollAndWaitForIdle();
+
+        // Verify ALL_MOBILE template gets all. 3g template counters do not increase.
+        assertUidTotal(sTemplateImsi1, UID_RED, 49L, 49L, 25L, 12L, 1);
+        assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0);
+        // Verify 4g template counts appended stats on existing entry and newly created entry.
+        assertUidTotal(template4g, UID_RED, 4L + 33L, 4L + 27L, 3L + 8L, 1L + 10L, 1);
+        // Verify 5g template doesn't get anything since no traffic is generated on 5g.
+        assertUidTotal(template5g, UID_RED, 0L, 0L, 0L, 0L, 0);
+
+        // 5g network comes online.
+        incrementCurrentTime(MINUTE_IN_MILLIS);
+        setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_NR);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                // Existing stats remains.
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+                        16L, 22L, 17L, 2L, 0L))
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE,
+                        33L, 27L, 8L, 10L, 1L))
+                // Add some traffic on 5g.
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+                5L, 13L, 31L, 9L, 2L)));
+        forcePollAndWaitForIdle();
+
+        // Verify ALL_MOBILE template gets all.
+        assertUidTotal(sTemplateImsi1, UID_RED, 54L, 62L, 56L, 21L, 3);
+        // 3g/4g template counters do not increase.
+        assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0);
+        assertUidTotal(template4g, UID_RED, 4L + 33L, 4L + 27L, 3L + 8L, 1L + 10L, 1);
+        // Verify 5g template gets the 5g count.
+        assertUidTotal(template5g, UID_RED, 5L, 13L, 31L, 9L, 2);
+    }
+
+    @Test
+    public void testMobileStatsOemManaged() throws Exception {
+        final NetworkTemplate templateOemPaid = new NetworkTemplate(MATCH_MOBILE_WILDCARD,
+                /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null,
+                METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_PAID,
+                SUBSCRIBER_ID_MATCH_RULE_EXACT);
+
+        final NetworkTemplate templateOemPrivate = new NetworkTemplate(MATCH_MOBILE_WILDCARD,
+                /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null,
+                METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_PRIVATE,
+                SUBSCRIBER_ID_MATCH_RULE_EXACT);
+
+        final NetworkTemplate templateOemAll = new NetworkTemplate(MATCH_MOBILE_WILDCARD,
+                /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null,
+                METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+                OEM_PAID | OEM_PRIVATE, SUBSCRIBER_ID_MATCH_RULE_EXACT);
+
+        final NetworkTemplate templateOemYes = new NetworkTemplate(MATCH_MOBILE_WILDCARD,
+                /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null,
+                METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_MANAGED_YES,
+                SUBSCRIBER_ID_MATCH_RULE_EXACT);
+
+        final NetworkTemplate templateOemNone = new NetworkTemplate(MATCH_MOBILE_WILDCARD,
+                /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null,
+                METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_MANAGED_NO,
+                SUBSCRIBER_ID_MATCH_RULE_EXACT);
+
+        // OEM_PAID network comes online.
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{
+                buildOemManagedMobileState(IMSI_1, false,
+                new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PAID})};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Create some traffic.
+        incrementCurrentTime(MINUTE_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+                        36L, 41L, 24L, 96L, 0L)));
+        forcePollAndWaitForIdle();
+
+        // OEM_PRIVATE network comes online.
+        states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false,
+                new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE})};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Create some traffic.
+        incrementCurrentTime(MINUTE_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+                        49L, 71L, 72L, 48L, 0L)));
+        forcePollAndWaitForIdle();
+
+        // OEM_PAID + OEM_PRIVATE network comes online.
+        states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false,
+                new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE,
+                          NetworkCapabilities.NET_CAPABILITY_OEM_PAID})};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Create some traffic.
+        incrementCurrentTime(MINUTE_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+                        57L, 86L, 83L, 93L, 0L)));
+        forcePollAndWaitForIdle();
+
+        // OEM_NONE network comes online.
+        states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false, new int[]{})};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Create some traffic.
+        incrementCurrentTime(MINUTE_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+                        29L, 73L, 34L, 31L, 0L)));
+        forcePollAndWaitForIdle();
+
+        // Verify OEM_PAID template gets only relevant stats.
+        assertUidTotal(templateOemPaid, UID_RED, 36L, 41L, 24L, 96L, 0);
+
+        // Verify OEM_PRIVATE template gets only relevant stats.
+        assertUidTotal(templateOemPrivate, UID_RED, 49L, 71L, 72L, 48L, 0);
+
+        // Verify OEM_PAID + OEM_PRIVATE template gets only relevant stats.
+        assertUidTotal(templateOemAll, UID_RED, 57L, 86L, 83L, 93L, 0);
+
+        // Verify OEM_NONE sees only non-OEM managed stats.
+        assertUidTotal(templateOemNone, UID_RED, 29L, 73L, 34L, 31L, 0);
+
+        // Verify OEM_MANAGED_YES sees all OEM managed stats.
+        assertUidTotal(templateOemYes, UID_RED,
+                36L + 49L + 57L,
+                41L + 71L + 86L,
+                24L + 72L + 83L,
+                96L + 48L + 93L, 0);
+
+        // Verify ALL_MOBILE template gets both OEM managed and non-OEM managed stats.
+        assertUidTotal(sTemplateImsi1, UID_RED,
+                36L + 49L + 57L + 29L,
+                41L + 71L + 86L + 73L,
+                24L + 72L + 83L + 34L,
+                96L + 48L + 93L + 31L, 0);
+    }
+
+    // TODO: support per IMSI state
+    private void setMobileRatTypeAndWaitForIdle(int ratType) {
+        when(mNetworkStatsSubscriptionsMonitor.getRatTypeForSubscriberId(anyString()))
+                .thenReturn(ratType);
+        mService.handleOnCollapsedRatTypeChanged();
+        HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
+    }
+
+    @Test
+    public void testSummaryForAllUid() throws Exception {
+        // pretend that network comes online
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // create some traffic for two apps
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 0L)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L));
+        mService.incrementOperationCount(UID_RED, 0xF00D, 1);
+
+        forcePollAndWaitForIdle();
+
+        // verify service recorded history
+        assertUidTotal(sTemplateWifi, UID_RED, 50L, 5L, 50L, 5L, 1);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 1024L, 8L, 512L, 4L, 0);
+
+
+        // now create more traffic in next hour, but only for one app
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 0L)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE,
+                        2048L, 16L, 1024L, 8L, 0L));
+        forcePollAndWaitForIdle();
+
+        // first verify entire history present
+        NetworkStats stats = mSession.getSummaryForAllUid(
+                sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true);
+        assertEquals(3, stats.size());
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 50L, 5L, 50L, 5L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 10L, 1L, 10L, 1L, 1);
+        assertValues(stats, IFACE_ALL, UID_BLUE, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 2048L, 16L, 1024L, 8L, 0);
+
+        // now verify that recent history only contains one uid
+        final long currentTime = currentTimeMillis();
+        stats = mSession.getSummaryForAllUid(
+                sTemplateWifi, currentTime - HOUR_IN_MILLIS, currentTime, true);
+        assertEquals(1, stats.size());
+        assertValues(stats, IFACE_ALL, UID_BLUE, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 1024L, 8L, 512L, 4L, 0);
+    }
+
+    @Test
+    public void testDetailedUidStats() throws Exception {
+        // pretend that network comes online
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        NetworkStats.Entry entry1 = new NetworkStats.Entry(
+                TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L);
+        NetworkStats.Entry entry2 = new NetworkStats.Entry(
+                TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 50L, 5L, 50L, 5L, 0L);
+        NetworkStats.Entry entry3 = new NetworkStats.Entry(
+                TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xBEEF, 1024L, 8L, 512L, 4L, 0L);
+
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+                .insertEntry(entry1)
+                .insertEntry(entry2)
+                .insertEntry(entry3));
+        mService.incrementOperationCount(UID_RED, 0xF00D, 1);
+
+        NetworkStats stats = mService.getDetailedUidStats(INTERFACES_ALL);
+
+        assertEquals(3, stats.size());
+        entry1.operations = 1;
+        assertEquals(entry1, stats.getValues(0, null));
+        entry2.operations = 1;
+        assertEquals(entry2, stats.getValues(1, null));
+        assertEquals(entry3, stats.getValues(2, null));
+    }
+
+    @Test
+    public void testDetailedUidStats_Filtered() throws Exception {
+        // pretend that network comes online
+        expectDefaultSettings();
+
+        final String stackedIface = "stacked-test0";
+        final LinkProperties stackedProp = new LinkProperties();
+        stackedProp.setInterfaceName(stackedIface);
+        final NetworkStateSnapshot wifiState = buildWifiState();
+        wifiState.getLinkProperties().addStackedLink(stackedProp);
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {wifiState};
+
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        NetworkStats.Entry uidStats = new NetworkStats.Entry(
+                TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xF00D, 1024L, 8L, 512L, 4L, 0L);
+        // Stacked on matching interface
+        NetworkStats.Entry tetheredStats1 = new NetworkStats.Entry(
+                stackedIface, UID_BLUE, SET_DEFAULT, 0xF00D, 1024L, 8L, 512L, 4L, 0L);
+        // Different interface
+        NetworkStats.Entry tetheredStats2 = new NetworkStats.Entry(
+                "otherif", UID_BLUE, SET_DEFAULT, 0xF00D, 1024L, 8L, 512L, 4L, 0L);
+
+        final String[] ifaceFilter = new String[] { TEST_IFACE };
+        final String[] augmentedIfaceFilter = new String[] { stackedIface, TEST_IFACE };
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        when(mStatsFactory.augmentWithStackedInterfaces(eq(ifaceFilter)))
+                .thenReturn(augmentedIfaceFilter);
+        when(mStatsFactory.readNetworkStatsDetail(eq(UID_ALL), any(), eq(TAG_ALL)))
+                .thenReturn(new NetworkStats(getElapsedRealtime(), 1)
+                        .insertEntry(uidStats));
+        when(mNetManager.getNetworkStatsTethering(STATS_PER_UID))
+                .thenReturn(new NetworkStats(getElapsedRealtime(), 2)
+                        .insertEntry(tetheredStats1)
+                        .insertEntry(tetheredStats2));
+
+        NetworkStats stats = mService.getDetailedUidStats(ifaceFilter);
+
+        // mStatsFactory#readNetworkStatsDetail() has the following invocations:
+        // 1) NetworkStatsService#systemReady from #setUp.
+        // 2) mService#notifyNetworkStatus in the test above.
+        //
+        // Additionally, we should have one call from the above call to mService#getDetailedUidStats
+        // with the augmented ifaceFilter.
+        verify(mStatsFactory, times(2)).readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL);
+        verify(mStatsFactory, times(1)).readNetworkStatsDetail(
+                eq(UID_ALL),
+                eq(augmentedIfaceFilter),
+                eq(TAG_ALL));
+        assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), TEST_IFACE));
+        assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), stackedIface));
+        assertEquals(2, stats.size());
+        assertEquals(uidStats, stats.getValues(0, null));
+        assertEquals(tetheredStats1, stats.getValues(1, null));
+    }
+
+    @Test
+    public void testForegroundBackground() throws Exception {
+        // pretend that network comes online
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // create some initial traffic
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L));
+        mService.incrementOperationCount(UID_RED, 0xF00D, 1);
+
+        forcePollAndWaitForIdle();
+
+        // verify service recorded history
+        assertUidTotal(sTemplateWifi, UID_RED, 128L, 2L, 128L, 2L, 1);
+
+
+        // now switch to foreground
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 32L, 2L, 32L, 2L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 1L, 1L, 1L, 1L, 0L));
+        mService.setUidForeground(UID_RED, true);
+        mService.incrementOperationCount(UID_RED, 0xFAAD, 1);
+
+        forcePollAndWaitForIdle();
+
+        // test that we combined correctly
+        assertUidTotal(sTemplateWifi, UID_RED, 160L, 4L, 160L, 4L, 2);
+
+        // verify entire history present
+        final NetworkStats stats = mSession.getSummaryForAllUid(
+                sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true);
+        assertEquals(4, stats.size());
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 32L, 2L, 32L, 2L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_FOREGROUND, 0xFAAD, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 1L, 1L, 1L, 1L, 1);
+    }
+
+    @Test
+    public void testMetered() throws Exception {
+        // pretend that network comes online
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states =
+                new NetworkStateSnapshot[] {buildWifiState(true /* isMetered */, TEST_IFACE)};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // create some initial traffic
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        // Note that all traffic from NetworkManagementService is tagged as METERED_NO, ROAMING_NO
+        // and DEFAULT_NETWORK_YES, because these three properties aren't tracked at that layer.
+        // We layer them on top by inspecting the iface properties.
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 0L));
+        mService.incrementOperationCount(UID_RED, 0xF00D, 1);
+
+        forcePollAndWaitForIdle();
+
+        // verify service recorded history
+        assertUidTotal(sTemplateWifi, UID_RED, 128L, 2L, 128L, 2L, 1);
+        // verify entire history present
+        final NetworkStats stats = mSession.getSummaryForAllUid(
+                sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true);
+        assertEquals(2, stats.size());
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                DEFAULT_NETWORK_YES,  128L, 2L, 128L, 2L, 1);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 1);
+    }
+
+    @Test
+    public void testRoaming() throws Exception {
+        // pretend that network comes online
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states =
+            new NetworkStateSnapshot[] {buildMobile3gState(IMSI_1, true /* isRoaming */)};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Create some traffic
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        // Note that all traffic from NetworkManagementService is tagged as METERED_NO and
+        // ROAMING_NO, because metered and roaming isn't tracked at that layer. We layer it
+        // on top by inspecting the iface properties.
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_ALL, ROAMING_NO,
+                        DEFAULT_NETWORK_YES,  128L, 2L, 128L, 2L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, METERED_ALL, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 0L));
+        forcePollAndWaitForIdle();
+
+        // verify service recorded history
+        assertUidTotal(sTemplateImsi1, UID_RED, 128L, 2L, 128L, 2L, 0);
+
+        // verify entire history present
+        final NetworkStats stats = mSession.getSummaryForAllUid(
+                sTemplateImsi1, Long.MIN_VALUE, Long.MAX_VALUE, true);
+        assertEquals(2, stats.size());
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_ALL, ROAMING_YES,
+                DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 0);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_ALL, ROAMING_YES,
+                DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 0);
+    }
+
+    @Test
+    public void testTethering() throws Exception {
+        // pretend first mobile network comes online
+        expectDefaultSettings();
+        final NetworkStateSnapshot[] states =
+                new NetworkStateSnapshot[]{buildMobile3gState(IMSI_1)};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // create some tethering traffic
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+
+        // Register custom provider and retrieve callback.
+        final TestableNetworkStatsProviderBinder provider =
+                new TestableNetworkStatsProviderBinder();
+        final INetworkStatsProviderCallback cb =
+                mService.registerNetworkStatsProvider("TEST-TETHERING-OFFLOAD", provider);
+        assertNotNull(cb);
+        final long now = getElapsedRealtime();
+
+        // Traffic seen by kernel counters (includes software tethering).
+        final NetworkStats swIfaceStats = new NetworkStats(now, 1)
+                .insertEntry(TEST_IFACE, 1536L, 12L, 384L, 3L);
+        // Hardware tethering traffic, not seen by kernel counters.
+        final NetworkStats tetherHwIfaceStats = new NetworkStats(now, 1)
+                .insertEntry(new NetworkStats.Entry(TEST_IFACE, UID_ALL, SET_DEFAULT,
+                        TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES,
+                        512L, 4L, 128L, 1L, 0L));
+        final NetworkStats tetherHwUidStats = new NetworkStats(now, 1)
+                .insertEntry(new NetworkStats.Entry(TEST_IFACE, UID_TETHERING, SET_DEFAULT,
+                        TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES,
+                        512L, 4L, 128L, 1L, 0L));
+        cb.notifyStatsUpdated(0 /* unused */, tetherHwIfaceStats, tetherHwUidStats);
+
+        // Fake some traffic done by apps on the device (as opposed to tethering), and record it
+        // into UID stats (as opposed to iface stats).
+        final NetworkStats localUidStats = new NetworkStats(now, 1)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L);
+        // Software per-uid tethering traffic.
+        final NetworkStats tetherSwUidStats = new NetworkStats(now, 1)
+                .insertEntry(TEST_IFACE, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1408L, 10L, 256L, 1L,
+                        0L);
+
+        expectNetworkStatsSummary(swIfaceStats);
+        expectNetworkStatsUidDetail(localUidStats, tetherSwUidStats);
+        forcePollAndWaitForIdle();
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0);
+        assertUidTotal(sTemplateImsi1, UID_RED, 128L, 2L, 128L, 2L, 0);
+        assertUidTotal(sTemplateImsi1, UID_TETHERING, 1920L, 14L, 384L, 2L, 0);
+    }
+
+    @Test
+    public void testRegisterUsageCallback() throws Exception {
+        // pretend that wifi network comes online; service should ask about full
+        // network state, and poll any existing interfaces before updating.
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // verify service has empty history for wifi
+        assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
+        long thresholdInBytes = 1L;  // very small; should be overriden by framework
+        DataUsageRequest inputRequest = new DataUsageRequest(
+                DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdInBytes);
+
+        // Create a messenger that waits for callback activity
+        ConditionVariable cv = new ConditionVariable(false);
+        LatchedHandler latchedHandler = new LatchedHandler(Looper.getMainLooper(), cv);
+        Messenger messenger = new Messenger(latchedHandler);
+
+        // Force poll
+        expectDefaultSettings();
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        // Register and verify request and that binder was called
+        DataUsageRequest request =
+                mService.registerUsageCallback(mServiceContext.getOpPackageName(), inputRequest,
+                        messenger, mBinder);
+        assertTrue(request.requestId > 0);
+        assertTrue(Objects.equals(sTemplateWifi, request.template));
+        long minThresholdInBytes = 2 * 1024 * 1024; // 2 MB
+        assertEquals(minThresholdInBytes, request.thresholdInBytes);
+
+        HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
+
+        // Make sure that the caller binder gets connected
+        verify(mBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+
+        // modify some number on wifi, and trigger poll event
+        // not enough traffic to call data usage callback
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 1024L, 1L, 2048L, 2L));
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        forcePollAndWaitForIdle();
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0);
+
+        // make sure callback has not being called
+        assertEquals(INVALID_TYPE, latchedHandler.lastMessageType);
+
+        // and bump forward again, with counters going higher. this is
+        // important, since it will trigger the data usage callback
+        incrementCurrentTime(DAY_IN_MILLIS);
+        expectDefaultSettings();
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 4096000L, 4L, 8192000L, 8L));
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        forcePollAndWaitForIdle();
+
+        // verify service recorded history
+        assertNetworkTotal(sTemplateWifi, 4096000L, 4L, 8192000L, 8L, 0);
+
+
+        // Wait for the caller to ack receipt of CALLBACK_LIMIT_REACHED
+        assertTrue(cv.block(WAIT_TIMEOUT));
+        assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, latchedHandler.lastMessageType);
+        cv.close();
+
+        // Allow binder to disconnect
+        when(mBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt())).thenReturn(true);
+
+        // Unregister request
+        mService.unregisterUsageRequest(request);
+
+        // Wait for the caller to ack receipt of CALLBACK_RELEASED
+        assertTrue(cv.block(WAIT_TIMEOUT));
+        assertEquals(NetworkStatsManager.CALLBACK_RELEASED, latchedHandler.lastMessageType);
+
+        // Make sure that the caller binder gets disconnected
+        verify(mBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+    }
+
+    @Test
+    public void testUnregisterUsageCallback_unknown_noop() throws Exception {
+        String callingPackage = "the.calling.package";
+        long thresholdInBytes = 10 * 1024 * 1024;  // 10 MB
+        DataUsageRequest unknownRequest = new DataUsageRequest(
+                2 /* requestId */, sTemplateImsi1, thresholdInBytes);
+
+        mService.unregisterUsageRequest(unknownRequest);
+    }
+
+    @Test
+    public void testStatsProviderUpdateStats() throws Exception {
+        // Pretend that network comes online.
+        expectDefaultSettings();
+        final NetworkStateSnapshot[] states =
+                new NetworkStateSnapshot[]{buildWifiState(true /* isMetered */, TEST_IFACE)};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        // Register custom provider and retrieve callback.
+        final TestableNetworkStatsProviderBinder provider =
+                new TestableNetworkStatsProviderBinder();
+        final INetworkStatsProviderCallback cb =
+                mService.registerNetworkStatsProvider("TEST", provider);
+        assertNotNull(cb);
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Verifies that one requestStatsUpdate will be called during iface update.
+        provider.expectOnRequestStatsUpdate(0 /* unused */);
+
+        // Create some initial traffic and report to the service.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        final NetworkStats expectedStats = new NetworkStats(0L, 1)
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT,
+                        TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES,
+                        128L, 2L, 128L, 2L, 1L))
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT,
+                        0xF00D, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES,
+                        64L, 1L, 64L, 1L, 1L));
+        cb.notifyStatsUpdated(0 /* unused */, expectedStats, expectedStats);
+
+        // Make another empty mutable stats object. This is necessary since the new NetworkStats
+        // object will be used to compare with the old one in NetworkStatsRecoder, two of them
+        // cannot be the same object.
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        forcePollAndWaitForIdle();
+
+        // Verifies that one requestStatsUpdate and setAlert will be called during polling.
+        provider.expectOnRequestStatsUpdate(0 /* unused */);
+        provider.expectOnSetAlert(MB_IN_BYTES);
+
+        // Verifies that service recorded history, does not verify uid tag part.
+        assertUidTotal(sTemplateWifi, UID_RED, 128L, 2L, 128L, 2L, 1);
+
+        // Verifies that onStatsUpdated updates the stats accordingly.
+        final NetworkStats stats = mSession.getSummaryForAllUid(
+                sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true);
+        assertEquals(2, stats.size());
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 1L);
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 1L);
+
+        // Verifies that unregister the callback will remove the provider from service.
+        cb.unregister();
+        forcePollAndWaitForIdle();
+        provider.assertNoCallback();
+    }
+
+    @Test
+    public void testDualVilteProviderStats() throws Exception {
+        // Pretend that network comes online.
+        expectDefaultSettings();
+        final int subId1 = 1;
+        final int subId2 = 2;
+        final NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{
+                buildImsState(IMSI_1, subId1, TEST_IFACE),
+                buildImsState(IMSI_2, subId2, TEST_IFACE2)};
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        // Register custom provider and retrieve callback.
+        final TestableNetworkStatsProviderBinder provider =
+                new TestableNetworkStatsProviderBinder();
+        final INetworkStatsProviderCallback cb =
+                mService.registerNetworkStatsProvider("TEST", provider);
+        assertNotNull(cb);
+
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Verifies that one requestStatsUpdate will be called during iface update.
+        provider.expectOnRequestStatsUpdate(0 /* unused */);
+
+        // Create some initial traffic and report to the service.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        final String vtIface1 = NetworkStats.IFACE_VT + subId1;
+        final String vtIface2 = NetworkStats.IFACE_VT + subId2;
+        final NetworkStats expectedStats = new NetworkStats(0L, 1)
+                .addEntry(new NetworkStats.Entry(vtIface1, UID_RED, SET_DEFAULT,
+                        TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES,
+                        128L, 2L, 128L, 2L, 1L))
+                .addEntry(new NetworkStats.Entry(vtIface2, UID_RED, SET_DEFAULT,
+                        TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES,
+                        64L, 1L, 64L, 1L, 1L));
+        cb.notifyStatsUpdated(0 /* unused */, expectedStats, expectedStats);
+
+        // Make another empty mutable stats object. This is necessary since the new NetworkStats
+        // object will be used to compare with the old one in NetworkStatsRecoder, two of them
+        // cannot be the same object.
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        forcePollAndWaitForIdle();
+
+        // Verifies that one requestStatsUpdate and setAlert will be called during polling.
+        provider.expectOnRequestStatsUpdate(0 /* unused */);
+        provider.expectOnSetAlert(MB_IN_BYTES);
+
+        // Verifies that service recorded history, does not verify uid tag part.
+        assertUidTotal(sTemplateImsi1, UID_RED, 128L, 2L, 128L, 2L, 1);
+
+        // Verifies that onStatsUpdated updates the stats accordingly.
+        NetworkStats stats = mSession.getSummaryForAllUid(
+                sTemplateImsi1, Long.MIN_VALUE, Long.MAX_VALUE, true);
+        assertEquals(1, stats.size());
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 1L);
+
+        stats = mSession.getSummaryForAllUid(
+                sTemplateImsi2, Long.MIN_VALUE, Long.MAX_VALUE, true);
+        assertEquals(1, stats.size());
+        assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO,
+                DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 1L);
+
+        // Verifies that unregister the callback will remove the provider from service.
+        cb.unregister();
+        forcePollAndWaitForIdle();
+        provider.assertNoCallback();
+    }
+
+    @Test
+    public void testStatsProviderSetAlert() throws Exception {
+        // Pretend that network comes online.
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states =
+                new NetworkStateSnapshot[]{buildWifiState(true /* isMetered */, TEST_IFACE)};
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Register custom provider and retrieve callback.
+        final TestableNetworkStatsProviderBinder provider =
+                new TestableNetworkStatsProviderBinder();
+        final INetworkStatsProviderCallback cb =
+                mService.registerNetworkStatsProvider("TEST", provider);
+        assertNotNull(cb);
+
+        // Simulates alert quota of the provider has been reached.
+        cb.notifyAlertReached();
+        HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
+
+        // Verifies that polling is triggered by alert reached.
+        provider.expectOnRequestStatsUpdate(0 /* unused */);
+        // Verifies that global alert will be re-armed.
+        provider.expectOnSetAlert(MB_IN_BYTES);
+    }
+
+    private void setCombineSubtypeEnabled(boolean enable) {
+        when(mSettings.getCombineSubtypeEnabled()).thenReturn(enable);
+        mHandler.post(() -> mContentObserver.onChange(false, Settings.Global
+                    .getUriFor(Settings.Global.NETSTATS_COMBINE_SUBTYPE_ENABLED)));
+        waitForIdle();
+        if (enable) {
+            verify(mNetworkStatsSubscriptionsMonitor).stop();
+        } else {
+            verify(mNetworkStatsSubscriptionsMonitor).start();
+        }
+    }
+
+    @Test
+    public void testDynamicWatchForNetworkRatTypeChanges() throws Exception {
+        // Build 3G template, type unknown template to get stats while network type is unknown
+        // and type all template to get the sum of all network type stats.
+        final NetworkTemplate template3g =
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS);
+        final NetworkTemplate templateUnknown =
+                buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        final NetworkTemplate templateAll =
+                buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL);
+        final NetworkStateSnapshot[] states =
+                new NetworkStateSnapshot[]{buildMobile3gState(IMSI_1)};
+
+        expectNetworkStatsSummary(buildEmptyStats());
+        expectNetworkStatsUidDetail(buildEmptyStats());
+
+        // 3G network comes online.
+        setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS);
+        mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Create some traffic.
+        incrementCurrentTime(MINUTE_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+                        12L, 18L, 14L, 1L, 0L)));
+        forcePollAndWaitForIdle();
+
+        // Since CombineSubtypeEnabled is false by default in unit test, the generated traffic
+        // will be split by RAT type. Verify 3G templates gets stats, while template with unknown
+        // RAT type gets nothing, and template with NETWORK_TYPE_ALL gets all stats.
+        assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0);
+        assertUidTotal(templateUnknown, UID_RED, 0L, 0L, 0L, 0L, 0);
+        assertUidTotal(templateAll, UID_RED, 12L, 18L, 14L, 1L, 0);
+
+        // Stop monitoring data usage per RAT type changes NetworkStatsService records data
+        // to {@link TelephonyManager#NETWORK_TYPE_UNKNOWN}.
+        setCombineSubtypeEnabled(true);
+
+        // Call handleOnCollapsedRatTypeChanged manually to simulate the callback fired
+        // when stopping monitor, this is needed by NetworkStatsService to trigger
+        // handleNotifyNetworkStatus.
+        mService.handleOnCollapsedRatTypeChanged();
+        HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
+        // Create some traffic.
+        incrementCurrentTime(MINUTE_IN_MILLIS);
+        // Append more traffic on existing snapshot.
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+                        12L + 4L, 18L + 4L, 14L + 3L, 1L + 1L, 0L))
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE,
+                        35L, 29L, 7L, 11L, 1L)));
+        forcePollAndWaitForIdle();
+
+        // Verify 3G counters do not increase, while template with unknown RAT type gets new
+        // traffic and template with NETWORK_TYPE_ALL gets all stats.
+        assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0);
+        assertUidTotal(templateUnknown, UID_RED, 4L + 35L, 4L + 29L, 3L + 7L, 1L + 11L, 1);
+        assertUidTotal(templateAll, UID_RED, 16L + 35L, 22L + 29L, 17L + 7L, 2L + 11L, 1);
+
+        // Start monitoring data usage per RAT type changes and NetworkStatsService records data
+        // by a granular subtype representative of the actual subtype
+        setCombineSubtypeEnabled(false);
+
+        mService.handleOnCollapsedRatTypeChanged();
+        HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
+        // Create some traffic.
+        incrementCurrentTime(MINUTE_IN_MILLIS);
+        // Append more traffic on existing snapshot.
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+                        22L, 26L, 19L, 5L, 0L))
+                .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE,
+                        35L, 29L, 7L, 11L, 1L)));
+        forcePollAndWaitForIdle();
+
+        // Verify traffic is split by RAT type, no increase on template with unknown RAT type
+        // and template with NETWORK_TYPE_ALL gets all stats.
+        assertUidTotal(template3g, UID_RED, 6L + 12L , 4L + 18L, 2L + 14L, 3L + 1L, 0);
+        assertUidTotal(templateUnknown, UID_RED, 4L + 35L, 4L + 29L, 3L + 7L, 1L + 11L, 1);
+        assertUidTotal(templateAll, UID_RED, 22L + 35L, 26L + 29L, 19L + 7L, 5L + 11L, 1);
+    }
+
+    @Test
+    public void testOperationCount_nonDefault_traffic() throws Exception {
+        // Pretend mobile network comes online, but wifi is the default network.
+        expectDefaultSettings();
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{
+                buildWifiState(true /*isMetered*/, TEST_IFACE2), buildMobile3gState(IMSI_1)};
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // Create some traffic on mobile network.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 4)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_NO, 2L, 1L, 3L, 4L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                        DEFAULT_NETWORK_YES, 1L, 3L, 2L, 1L, 0L)
+                .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 5L, 4L, 1L, 4L, 0L));
+        // Increment operation count, which must have a specific tag.
+        mService.incrementOperationCount(UID_RED, 0xF00D, 2);
+        forcePollAndWaitForIdle();
+
+        // Verify mobile summary is not changed by the operation count.
+        final NetworkTemplate templateMobile =
+                buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL);
+        final NetworkStats statsMobile = mSession.getSummaryForAllUid(
+                templateMobile, Long.MIN_VALUE, Long.MAX_VALUE, true);
+        assertValues(statsMobile, IFACE_ALL, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 3L, 4L, 5L, 5L, 0);
+        assertValues(statsMobile, IFACE_ALL, UID_RED, SET_ALL, 0xF00D, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 5L, 4L, 1L, 4L, 0);
+
+        // Verify the operation count is blamed onto the default network.
+        // TODO: Blame onto the default network is not very reasonable. Consider blame onto the
+        //  network that generates the traffic.
+        final NetworkTemplate templateWifi = buildTemplateWifiWildcard();
+        final NetworkStats statsWifi = mSession.getSummaryForAllUid(
+                templateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true);
+        assertValues(statsWifi, IFACE_ALL, UID_RED, SET_ALL, 0xF00D, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 0L, 0L, 0L, 0L, 2);
+    }
+
+    private static File getBaseDir(File statsDir) {
+        File baseDir = new File(statsDir, "netstats");
+        baseDir.mkdirs();
+        return baseDir;
+    }
+
+    private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets,
+            long txBytes, long txPackets, int operations) throws Exception {
+        assertNetworkTotal(template, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes,
+                txPackets, operations);
+    }
+
+    private void assertNetworkTotal(NetworkTemplate template, long start, long end, long rxBytes,
+            long rxPackets, long txBytes, long txPackets, int operations) throws Exception {
+        // verify history API
+        final NetworkStatsHistory history = mSession.getHistoryForNetwork(template, FIELD_ALL);
+        assertValues(history, start, end, rxBytes, rxPackets, txBytes, txPackets, operations);
+
+        // verify summary API
+        final NetworkStats stats = mSession.getSummaryForNetwork(template, start, end);
+        assertValues(stats, IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL,  rxBytes, rxPackets, txBytes, txPackets, operations);
+    }
+
+    private void assertUidTotal(NetworkTemplate template, int uid, long rxBytes, long rxPackets,
+            long txBytes, long txPackets, int operations) throws Exception {
+        assertUidTotal(template, uid, SET_ALL, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL,
+                rxBytes, rxPackets, txBytes, txPackets, operations);
+    }
+
+    private void assertUidTotal(NetworkTemplate template, int uid, int set, int metered,
+            int roaming, int defaultNetwork, long rxBytes, long rxPackets, long txBytes,
+            long txPackets, int operations) throws Exception {
+        // verify history API
+        final NetworkStatsHistory history = mSession.getHistoryForUid(
+                template, uid, set, TAG_NONE, FIELD_ALL);
+        assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes,
+                txPackets, operations);
+
+        // verify summary API
+        final NetworkStats stats = mSession.getSummaryForAllUid(
+                template, Long.MIN_VALUE, Long.MAX_VALUE, false);
+        assertValues(stats, IFACE_ALL, uid, set, TAG_NONE, metered, roaming, defaultNetwork,
+                rxBytes, rxPackets, txBytes, txPackets, operations);
+    }
+
+    private void expectSystemReady() throws Exception {
+        expectNetworkStatsSummary(buildEmptyStats());
+    }
+
+    private String getActiveIface(NetworkStateSnapshot... states) throws Exception {
+        if (states == null || states.length == 0 || states[0].getLinkProperties() == null) {
+            return null;
+        }
+        return states[0].getLinkProperties().getInterfaceName();
+    }
+
+    private void expectNetworkStatsSummary(NetworkStats summary) throws Exception {
+        expectNetworkStatsSummaryDev(summary.clone());
+        expectNetworkStatsSummaryXt(summary.clone());
+    }
+
+    private void expectNetworkStatsSummaryDev(NetworkStats summary) throws Exception {
+        when(mStatsFactory.readNetworkStatsSummaryDev()).thenReturn(summary);
+    }
+
+    private void expectNetworkStatsSummaryXt(NetworkStats summary) throws Exception {
+        when(mStatsFactory.readNetworkStatsSummaryXt()).thenReturn(summary);
+    }
+
+    private void expectNetworkStatsUidDetail(NetworkStats detail) throws Exception {
+        expectNetworkStatsUidDetail(detail, new NetworkStats(0L, 0));
+    }
+
+    private void expectNetworkStatsUidDetail(NetworkStats detail, NetworkStats tetherStats)
+            throws Exception {
+        when(mStatsFactory.readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL))
+                .thenReturn(detail);
+
+        // also include tethering details, since they are folded into UID
+        when(mNetManager.getNetworkStatsTethering(STATS_PER_UID)).thenReturn(tetherStats);
+    }
+
+    private void expectDefaultSettings() throws Exception {
+        expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS);
+    }
+
+    private void expectSettings(long persistBytes, long bucketDuration, long deleteAge)
+            throws Exception {
+        when(mSettings.getPollInterval()).thenReturn(HOUR_IN_MILLIS);
+        when(mSettings.getPollDelay()).thenReturn(0L);
+        when(mSettings.getSampleEnabled()).thenReturn(true);
+        when(mSettings.getCombineSubtypeEnabled()).thenReturn(false);
+
+        final Config config = new Config(bucketDuration, deleteAge, deleteAge);
+        when(mSettings.getDevConfig()).thenReturn(config);
+        when(mSettings.getXtConfig()).thenReturn(config);
+        when(mSettings.getUidConfig()).thenReturn(config);
+        when(mSettings.getUidTagConfig()).thenReturn(config);
+
+        when(mSettings.getGlobalAlertBytes(anyLong())).thenReturn(MB_IN_BYTES);
+        when(mSettings.getDevPersistBytes(anyLong())).thenReturn(MB_IN_BYTES);
+        when(mSettings.getXtPersistBytes(anyLong())).thenReturn(MB_IN_BYTES);
+        when(mSettings.getUidPersistBytes(anyLong())).thenReturn(MB_IN_BYTES);
+        when(mSettings.getUidTagPersistBytes(anyLong())).thenReturn(MB_IN_BYTES);
+    }
+
+    private void assertStatsFilesExist(boolean exist) {
+        final File basePath = new File(mStatsDir, "netstats");
+        if (exist) {
+            assertTrue(basePath.list().length > 0);
+        } else {
+            assertTrue(basePath.list().length == 0);
+        }
+    }
+
+    private static void assertValues(NetworkStatsHistory stats, long start, long end, long rxBytes,
+            long rxPackets, long txBytes, long txPackets, int operations) {
+        final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null);
+        assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes);
+        assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets);
+        assertEquals("unexpected txBytes", txBytes, entry.txBytes);
+        assertEquals("unexpected txPackets", txPackets, entry.txPackets);
+        assertEquals("unexpected operations", operations, entry.operations);
+    }
+
+    private static NetworkStateSnapshot buildWifiState() {
+        return buildWifiState(false, TEST_IFACE, null);
+    }
+
+    private static NetworkStateSnapshot buildWifiState(boolean isMetered, @NonNull String iface) {
+        return buildWifiState(isMetered, iface, null);
+    }
+
+    private static NetworkStateSnapshot buildWifiState(boolean isMetered, @NonNull String iface,
+            String subscriberId) {
+        final LinkProperties prop = new LinkProperties();
+        prop.setInterfaceName(iface);
+        final NetworkCapabilities capabilities = new NetworkCapabilities();
+        capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, !isMetered);
+        capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true);
+        capabilities.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
+        capabilities.setSSID(TEST_SSID);
+        return new NetworkStateSnapshot(WIFI_NETWORK, capabilities, prop, subscriberId, TYPE_WIFI);
+    }
+
+    private static NetworkStateSnapshot buildMobile3gState(String subscriberId) {
+        return buildMobile3gState(subscriberId, false /* isRoaming */);
+    }
+
+    private static NetworkStateSnapshot buildMobile3gState(String subscriberId, boolean isRoaming) {
+        final LinkProperties prop = new LinkProperties();
+        prop.setInterfaceName(TEST_IFACE);
+        final NetworkCapabilities capabilities = new NetworkCapabilities();
+        capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false);
+        capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, !isRoaming);
+        capabilities.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        return new NetworkStateSnapshot(
+                MOBILE_NETWORK, capabilities, prop, subscriberId, TYPE_MOBILE);
+    }
+
+    private NetworkStats buildEmptyStats() {
+        return new NetworkStats(getElapsedRealtime(), 0);
+    }
+
+    private static NetworkStateSnapshot buildOemManagedMobileState(
+            String subscriberId, boolean isRoaming, int[] oemNetCapabilities) {
+        final LinkProperties prop = new LinkProperties();
+        prop.setInterfaceName(TEST_IFACE);
+        final NetworkCapabilities capabilities = new NetworkCapabilities();
+        capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false);
+        capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, !isRoaming);
+        for (int nc : oemNetCapabilities) {
+            capabilities.setCapability(nc, true);
+        }
+        capabilities.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        return new NetworkStateSnapshot(MOBILE_NETWORK, capabilities, prop, subscriberId,
+                TYPE_MOBILE);
+    }
+
+    private static NetworkStateSnapshot buildImsState(
+            String subscriberId, int subId, String ifaceName) {
+        final LinkProperties prop = new LinkProperties();
+        prop.setInterfaceName(ifaceName);
+        final NetworkCapabilities capabilities = new NetworkCapabilities();
+        capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true);
+        capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true);
+        capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_IMS, true);
+        capabilities.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        capabilities.setNetworkSpecifier(new TelephonyNetworkSpecifier(subId));
+        return new NetworkStateSnapshot(
+                MOBILE_NETWORK, capabilities, prop, subscriberId, TYPE_MOBILE);
+    }
+
+    private long getElapsedRealtime() {
+        return mElapsedRealtime;
+    }
+
+    private long startTimeMillis() {
+        return TEST_START;
+    }
+
+    private long currentTimeMillis() {
+        return startTimeMillis() + mElapsedRealtime;
+    }
+
+    private void incrementCurrentTime(long duration) {
+        mElapsedRealtime += duration;
+    }
+
+    private void forcePollAndWaitForIdle() {
+        mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+        waitForIdle();
+    }
+
+    private void waitForIdle() {
+        HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
+    }
+
+    static class LatchedHandler extends Handler {
+        private final ConditionVariable mCv;
+        int lastMessageType = INVALID_TYPE;
+
+        LatchedHandler(Looper looper, ConditionVariable cv) {
+            super(looper);
+            mCv = cv;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            lastMessageType = msg.what;
+            mCv.open();
+            super.handleMessage(msg);
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
new file mode 100644
index 0000000..6d2c7dc
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
@@ -0,0 +1,386 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.NetworkTemplate;
+import android.os.test.TestLooper;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.util.CollectionUtils;
+import com.android.server.net.NetworkStatsSubscriptionsMonitor.RatTypeListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+@RunWith(JUnit4.class)
+public final class NetworkStatsSubscriptionsMonitorTest {
+    private static final int TEST_SUBID1 = 3;
+    private static final int TEST_SUBID2 = 5;
+    private static final String TEST_IMSI1 = "466921234567890";
+    private static final String TEST_IMSI2 = "466920987654321";
+    private static final String TEST_IMSI3 = "466929999999999";
+
+    @Mock private Context mContext;
+    @Mock private SubscriptionManager mSubscriptionManager;
+    @Mock private TelephonyManager mTelephonyManager;
+    @Mock private NetworkStatsSubscriptionsMonitor.Delegate mDelegate;
+    private final List<Integer> mTestSubList = new ArrayList<>();
+
+    private final Executor mExecutor = Executors.newSingleThreadExecutor();
+    private NetworkStatsSubscriptionsMonitor mMonitor;
+    private TestLooper mTestLooper = new TestLooper();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager);
+
+        when(mContext.getSystemService(eq(Context.TELEPHONY_SUBSCRIPTION_SERVICE)))
+                .thenReturn(mSubscriptionManager);
+        when(mContext.getSystemService(eq(Context.TELEPHONY_SERVICE)))
+                .thenReturn(mTelephonyManager);
+
+        mMonitor = new NetworkStatsSubscriptionsMonitor(mContext, mTestLooper.getLooper(),
+                mExecutor, mDelegate);
+    }
+
+    @Test
+    public void testStartStop() {
+        // Verify that addOnSubscriptionsChangedListener() is never called before start().
+        verify(mSubscriptionManager, never())
+                .addOnSubscriptionsChangedListener(mExecutor, mMonitor);
+        mMonitor.start();
+        verify(mSubscriptionManager).addOnSubscriptionsChangedListener(mExecutor, mMonitor);
+
+        // Verify that removeOnSubscriptionsChangedListener() is never called before stop()
+        verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(mMonitor);
+        mMonitor.stop();
+        verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(mMonitor);
+    }
+
+    @NonNull
+    private static int[] convertArrayListToIntArray(@NonNull List<Integer> arrayList) {
+        final int[] list = new int[arrayList.size()];
+        for (int i = 0; i < arrayList.size(); i++) {
+            list[i] = arrayList.get(i);
+        }
+        return list;
+    }
+
+    private void setRatTypeForSub(List<RatTypeListener> listeners,
+            int subId, int type) {
+        final ServiceState serviceState = mock(ServiceState.class);
+        when(serviceState.getDataNetworkType()).thenReturn(type);
+        final RatTypeListener match = CollectionUtils
+                .find(listeners, it -> it.getSubId() == subId);
+        if (match == null) {
+            fail("Could not find listener with subId: " + subId);
+        }
+        match.onServiceStateChanged(serviceState);
+    }
+
+    private void addTestSub(int subId, String subscriberId) {
+        // add SubId to TestSubList.
+        if (mTestSubList.contains(subId)) fail("The subscriber list already contains this ID");
+
+        mTestSubList.add(subId);
+
+        final int[] subList = convertArrayListToIntArray(mTestSubList);
+        when(mSubscriptionManager.getCompleteActiveSubscriptionIdList()).thenReturn(subList);
+        when(mTelephonyManager.getSubscriberId(subId)).thenReturn(subscriberId);
+        mMonitor.onSubscriptionsChanged();
+    }
+
+    private void updateSubscriberIdForTestSub(int subId, @Nullable final String subscriberId) {
+        when(mTelephonyManager.getSubscriberId(subId)).thenReturn(subscriberId);
+        mMonitor.onSubscriptionsChanged();
+    }
+
+    private void removeTestSub(int subId) {
+        // Remove subId from TestSubList.
+        mTestSubList.removeIf(it -> it == subId);
+        final int[] subList = convertArrayListToIntArray(mTestSubList);
+        when(mSubscriptionManager.getCompleteActiveSubscriptionIdList()).thenReturn(subList);
+        mMonitor.onSubscriptionsChanged();
+    }
+
+    private void assertRatTypeChangedForSub(String subscriberId, int ratType) {
+        assertEquals(ratType, mMonitor.getRatTypeForSubscriberId(subscriberId));
+        final ArgumentCaptor<Integer> typeCaptor = ArgumentCaptor.forClass(Integer.class);
+        // Verify callback with the subscriberId and the RAT type should be as expected.
+        // It will fail if get a callback with an unexpected RAT type.
+        verify(mDelegate).onCollapsedRatTypeChanged(eq(subscriberId), typeCaptor.capture());
+        final int type = typeCaptor.getValue();
+        assertEquals(ratType, type);
+    }
+
+    private void assertRatTypeNotChangedForSub(String subscriberId, int ratType) {
+        assertEquals(mMonitor.getRatTypeForSubscriberId(subscriberId), ratType);
+        // Should never get callback with any RAT type.
+        verify(mDelegate, never()).onCollapsedRatTypeChanged(eq(subscriberId), anyInt());
+    }
+
+    @Test
+    public void testSubChangedAndRatTypeChanged() {
+        final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor =
+                ArgumentCaptor.forClass(RatTypeListener.class);
+
+        mMonitor.start();
+        // Insert sim1, verify RAT type is NETWORK_TYPE_UNKNOWN, and never get any callback
+        // before changing RAT type.
+        addTestSub(TEST_SUBID1, TEST_IMSI1);
+        assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+
+        // Insert sim2.
+        addTestSub(TEST_SUBID2, TEST_IMSI2);
+        assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        verify(mTelephonyManager, times(2)).listen(ratTypeListenerCaptor.capture(),
+                eq(PhoneStateListener.LISTEN_SERVICE_STATE));
+        reset(mDelegate);
+
+        // Set RAT type of sim1 to UMTS.
+        // Verify RAT type of sim1 after subscription gets onCollapsedRatTypeChanged() callback
+        // and others remain untouched.
+        setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1,
+                TelephonyManager.NETWORK_TYPE_UMTS);
+        assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS);
+        assertRatTypeNotChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        reset(mDelegate);
+
+        // Set RAT type of sim2 to LTE.
+        // Verify RAT type of sim2 after subscription gets onCollapsedRatTypeChanged() callback
+        // and others remain untouched.
+        setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID2,
+                TelephonyManager.NETWORK_TYPE_LTE);
+        assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS);
+        assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_LTE);
+        assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        reset(mDelegate);
+
+        // Remove sim2 and verify that callbacks are fired and RAT type is correct for sim2.
+        // while the other two remain untouched.
+        removeTestSub(TEST_SUBID2);
+        verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
+        assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS);
+        assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        reset(mDelegate);
+
+        // Set RAT type of sim1 to UNKNOWN. Then stop monitoring subscription changes
+        // and verify that the listener for sim1 is removed.
+        setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1,
+                TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        reset(mDelegate);
+
+        mMonitor.stop();
+        verify(mTelephonyManager, times(2)).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
+        assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+    }
+
+
+    @Test
+    public void test5g() {
+        mMonitor.start();
+        // Insert sim1, verify RAT type is NETWORK_TYPE_UNKNOWN, and never get any callback
+        // before changing RAT type. Also capture listener for later use.
+        addTestSub(TEST_SUBID1, TEST_IMSI1);
+        assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor =
+                ArgumentCaptor.forClass(RatTypeListener.class);
+        verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor.capture(),
+                eq(PhoneStateListener.LISTEN_SERVICE_STATE));
+        final RatTypeListener listener = CollectionUtils
+                .find(ratTypeListenerCaptor.getAllValues(), it -> it.getSubId() == TEST_SUBID1);
+        assertNotNull(listener);
+
+        // Set RAT type to 5G NSA (non-standalone) mode, verify the monitor outputs
+        // NETWORK_TYPE_5G_NSA.
+        final ServiceState serviceState = mock(ServiceState.class);
+        when(serviceState.getDataNetworkType()).thenReturn(TelephonyManager.NETWORK_TYPE_LTE);
+        when(serviceState.getNrState()).thenReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED);
+        listener.onServiceStateChanged(serviceState);
+        assertRatTypeChangedForSub(TEST_IMSI1, NetworkTemplate.NETWORK_TYPE_5G_NSA);
+        reset(mDelegate);
+
+        // Set RAT type to LTE without NR connected, the RAT type should be downgraded to LTE.
+        when(serviceState.getNrState()).thenReturn(NetworkRegistrationInfo.NR_STATE_NONE);
+        listener.onServiceStateChanged(serviceState);
+        assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_LTE);
+        reset(mDelegate);
+
+        // Verify NR connected with other RAT type does not take effect.
+        when(serviceState.getDataNetworkType()).thenReturn(TelephonyManager.NETWORK_TYPE_UMTS);
+        when(serviceState.getNrState()).thenReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED);
+        listener.onServiceStateChanged(serviceState);
+        assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS);
+        reset(mDelegate);
+
+        // Set RAT type to 5G standalone mode, the RAT type should be NR.
+        setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1,
+                TelephonyManager.NETWORK_TYPE_NR);
+        assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_NR);
+        reset(mDelegate);
+
+        // Set NR state to none in standalone mode does not change anything.
+        when(serviceState.getDataNetworkType()).thenReturn(TelephonyManager.NETWORK_TYPE_NR);
+        when(serviceState.getNrState()).thenReturn(NetworkRegistrationInfo.NR_STATE_NONE);
+        listener.onServiceStateChanged(serviceState);
+        assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_NR);
+    }
+
+    @Test
+    public void testSubscriberIdUnavailable() {
+        final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor =
+                ArgumentCaptor.forClass(RatTypeListener.class);
+
+        mMonitor.start();
+        // Insert sim1, set subscriberId to null which is normal in SIM PIN locked case.
+        // Verify RAT type is NETWORK_TYPE_UNKNOWN and service will not perform listener
+        // registration.
+        addTestSub(TEST_SUBID1, null);
+        verify(mTelephonyManager, never()).listen(any(), anyInt());
+        assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+
+        // Set IMSI for sim1, verify the listener will be registered.
+        updateSubscriberIdForTestSub(TEST_SUBID1, TEST_IMSI1);
+        verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor.capture(),
+                eq(PhoneStateListener.LISTEN_SERVICE_STATE));
+        reset(mTelephonyManager);
+        when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager);
+
+        // Set RAT type of sim1 to UMTS. Verify RAT type of sim1 is changed.
+        setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1,
+                TelephonyManager.NETWORK_TYPE_UMTS);
+        assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS);
+        reset(mDelegate);
+
+        // Set IMSI to null again to simulate somehow IMSI is not available, such as
+        // modem crash. Verify service should unregister listener.
+        updateSubscriberIdForTestSub(TEST_SUBID1, null);
+        verify(mTelephonyManager, times(1)).listen(eq(ratTypeListenerCaptor.getValue()),
+                eq(PhoneStateListener.LISTEN_NONE));
+        assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        reset(mDelegate);
+        clearInvocations(mTelephonyManager);
+
+        // Simulate somehow IMSI is back. Verify service will register with
+        // another listener and fire callback accordingly.
+        final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor2 =
+                ArgumentCaptor.forClass(RatTypeListener.class);
+        updateSubscriberIdForTestSub(TEST_SUBID1, TEST_IMSI1);
+        verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor2.capture(),
+                eq(PhoneStateListener.LISTEN_SERVICE_STATE));
+        assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        reset(mDelegate);
+        clearInvocations(mTelephonyManager);
+
+        // Set RAT type of sim1 to LTE. Verify RAT type of sim1 still works.
+        setRatTypeForSub(ratTypeListenerCaptor2.getAllValues(), TEST_SUBID1,
+                TelephonyManager.NETWORK_TYPE_LTE);
+        assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_LTE);
+        reset(mDelegate);
+
+        mMonitor.stop();
+        verify(mTelephonyManager, times(1)).listen(eq(ratTypeListenerCaptor2.getValue()),
+                eq(PhoneStateListener.LISTEN_NONE));
+        assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+    }
+
+    /**
+     * Verify that when IMSI suddenly changed for a given subId, the service will register a new
+     * listener and unregister the old one, and report changes on updated IMSI. This is for modem
+     * feature that may be enabled for certain carrier, which changes to use a different IMSI while
+     * roaming on certain networks for multi-IMSI SIM cards, but the subId stays the same.
+     */
+    @Test
+    public void testSubscriberIdChanged() {
+        mMonitor.start();
+        // Insert sim1, verify RAT type is NETWORK_TYPE_UNKNOWN, and never get any callback
+        // before changing RAT type.
+        addTestSub(TEST_SUBID1, TEST_IMSI1);
+        final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor =
+                ArgumentCaptor.forClass(RatTypeListener.class);
+        verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor.capture(),
+                eq(PhoneStateListener.LISTEN_SERVICE_STATE));
+        assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+
+        // Set RAT type of sim1 to UMTS.
+        // Verify RAT type of sim1 changes accordingly.
+        setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1,
+                TelephonyManager.NETWORK_TYPE_UMTS);
+        assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS);
+        reset(mDelegate);
+        clearInvocations(mTelephonyManager);
+
+        // Simulate IMSI of sim1 changed to IMSI2. Verify the service will register with
+        // another listener and remove the old one. The RAT type of new IMSI stays at
+        // NETWORK_TYPE_UNKNOWN until received initial callback from telephony.
+        final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor2 =
+                ArgumentCaptor.forClass(RatTypeListener.class);
+        updateSubscriberIdForTestSub(TEST_SUBID1, TEST_IMSI2);
+        verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor2.capture(),
+                eq(PhoneStateListener.LISTEN_SERVICE_STATE));
+        verify(mTelephonyManager, times(1)).listen(eq(ratTypeListenerCaptor.getValue()),
+                eq(PhoneStateListener.LISTEN_NONE));
+        assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        assertRatTypeNotChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        reset(mDelegate);
+
+        // Set RAT type of sim1 to UMTS for new listener to simulate the initial callback received
+        // from telephony after registration. Verify RAT type of sim1 changes with IMSI2
+        // accordingly.
+        setRatTypeForSub(ratTypeListenerCaptor2.getAllValues(), TEST_SUBID1,
+                TelephonyManager.NETWORK_TYPE_UMTS);
+        assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+        assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UMTS);
+        reset(mDelegate);
+    }
+}
diff --git a/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java b/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java
new file mode 100644
index 0000000..ebbc0ef
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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 com.android.server.net.ipmemorystore;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.ipmemorystore.NetworkAttributes;
+import android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirk;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+
+/** Unit tests for {@link NetworkAttributes}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NetworkAttributesTest {
+    private static final String WEIGHT_FIELD_NAME_PREFIX = "WEIGHT_";
+    private static final float EPSILON = 0.0001f;
+
+    // This is running two tests to make sure the total weight is the sum of all weights. To be
+    // sure this is not fireproof, but you'd kind of need to do it on purpose to pass.
+    @Test
+    public void testTotalWeight() throws IllegalAccessException, UnknownHostException {
+        // Make sure that TOTAL_WEIGHT is equal to the sum of the fields starting with WEIGHT_
+        float sum = 0f;
+        final Field[] fieldList = NetworkAttributes.class.getDeclaredFields();
+        for (final Field field : fieldList) {
+            if (!field.getName().startsWith(WEIGHT_FIELD_NAME_PREFIX)) continue;
+            field.setAccessible(true);
+            sum += (float) field.get(null);
+        }
+        assertEquals(sum, NetworkAttributes.TOTAL_WEIGHT, EPSILON);
+
+        final IPv6ProvisioningLossQuirk ipv6ProvisioningLossQuirk =
+                new IPv6ProvisioningLossQuirk(3, System.currentTimeMillis() + 7_200_000);
+        // Use directly the constructor with all attributes, and make sure that when compared
+        // to itself the score is a clean 1.0f.
+        final NetworkAttributes na =
+                new NetworkAttributes(
+                        (Inet4Address) Inet4Address.getByAddress(new byte[] {1, 2, 3, 4}),
+                        System.currentTimeMillis() + 7_200_000,
+                        "some hint",
+                        Arrays.asList(Inet4Address.getByAddress(new byte[] {5, 6, 7, 8}),
+                                Inet4Address.getByAddress(new byte[] {9, 0, 1, 2})),
+                        98, ipv6ProvisioningLossQuirk);
+        assertEquals(1.0f, na.getNetworkGroupSamenessConfidence(na), EPSILON);
+    }
+}
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
new file mode 100644
index 0000000..1c1ba9e
--- /dev/null
+++ b/tests/unit/jni/Android.bp
@@ -0,0 +1,28 @@
+package {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_shared {
+    name: "libnetworkstatsfactorytestjni",
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+
+    srcs: [
+        ":lib_networkStatsFactory_native",
+        "test_onload.cpp",
+    ],
+
+    shared_libs: [
+        "libbpf_android",
+        "liblog",
+        "libnativehelper",
+        "libnetdbpf",
+        "libnetdutils",
+    ],
+}
diff --git a/tests/unit/jni/test_onload.cpp b/tests/unit/jni/test_onload.cpp
new file mode 100644
index 0000000..5194ddb
--- /dev/null
+++ b/tests/unit/jni/test_onload.cpp
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+/*
+ * this is a mini native libaray for NetworkStatsFactoryTest to run properly. It
+ * load all the native method related to NetworkStatsFactory when test run
+ */
+#include <nativehelper/JNIHelp.h>
+#include "jni.h"
+#include "utils/Log.h"
+#include "utils/misc.h"
+
+namespace android {
+int register_android_server_net_NetworkStatsFactory(JNIEnv* env);
+};
+
+using namespace android;
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
+{
+    JNIEnv* env = NULL;
+    jint result = -1;
+
+    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
+        ALOGE("GetEnv failed!");
+        return result;
+    }
+    ALOG_ASSERT(env, "Could not retrieve the env!");
+    register_android_server_net_NetworkStatsFactory(env);
+    return JNI_VERSION_1_4;
+}
diff --git a/tests/unit/res/raw/history_v1 b/tests/unit/res/raw/history_v1
new file mode 100644
index 0000000..de79491
--- /dev/null
+++ b/tests/unit/res/raw/history_v1
Binary files differ
diff --git a/tests/unit/res/raw/net_dev_typical b/tests/unit/res/raw/net_dev_typical
new file mode 100644
index 0000000..290bf03
--- /dev/null
+++ b/tests/unit/res/raw/net_dev_typical
@@ -0,0 +1,8 @@
+Inter-|   Receive                                                |  Transmit
+ face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
+    lo:    8308     116    0    0    0     0          0         0     8308     116    0    0    0     0       0          0
+rmnet0: 1507570    2205    0    0    0     0          0         0   489339    2237    0    0    0     0       0          0
+  ifb0:   52454     151    0  151    0     0          0         0        0       0    0    0    0     0       0          0
+  ifb1:   52454     151    0  151    0     0          0         0        0       0    0    0    0     0       0          0
+  sit0:       0       0    0    0    0     0          0         0        0       0  148    0    0     0       0          0
+ip6tnl0:       0       0    0    0    0     0          0         0        0       0  151  151    0     0       0          0
diff --git a/tests/unit/res/raw/netstats_uid_v4 b/tests/unit/res/raw/netstats_uid_v4
new file mode 100644
index 0000000..e75fc1c
--- /dev/null
+++ b/tests/unit/res/raw/netstats_uid_v4
Binary files differ
diff --git a/tests/unit/res/raw/netstats_v1 b/tests/unit/res/raw/netstats_v1
new file mode 100644
index 0000000..e80860a
--- /dev/null
+++ b/tests/unit/res/raw/netstats_v1
Binary files differ
diff --git a/tests/unit/res/raw/xt_qtaguid_iface_fmt_typical b/tests/unit/res/raw/xt_qtaguid_iface_fmt_typical
new file mode 100644
index 0000000..656d5bb
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_iface_fmt_typical
@@ -0,0 +1,4 @@
+ifname total_skb_rx_bytes total_skb_rx_packets total_skb_tx_bytes total_skb_tx_packets
+rmnet2 4968 35 3081 39
+rmnet1 11153922 8051 190226 2468
+rmnet0 6824 16 5692 10
diff --git a/tests/unit/res/raw/xt_qtaguid_iface_typical b/tests/unit/res/raw/xt_qtaguid_iface_typical
new file mode 100644
index 0000000..610723a
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_iface_typical
@@ -0,0 +1,6 @@
+rmnet3 1 0 0 0 0 20822 501 1149991 815
+rmnet2 1 0 0 0 0 1594 15 1313 15
+rmnet1 1 0 0 0 0 207398 458 166918 565
+rmnet0 1 0 0 0 0 2112 24 700 10
+test1 1 1 2 3 4 5 6 7 8
+test2 0 1 2 3 4 5 6 7 8
diff --git a/tests/unit/res/raw/xt_qtaguid_typical b/tests/unit/res/raw/xt_qtaguid_typical
new file mode 100644
index 0000000..c1b0d25
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_typical
@@ -0,0 +1,71 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 wlan0 0x0 0 0 18621 96 2898 44 312 6 15897 58 2412 32 312 6 1010 16 1576 22
+3 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+4 wlan0 0x0 1000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+5 wlan0 0x0 1000 1 1949 13 1078 14 0 0 1600 10 349 3 0 0 600 10 478 4
+6 wlan0 0x0 10005 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+7 wlan0 0x0 10005 1 32081 38 5315 50 32081 38 0 0 0 0 5315 50 0 0 0 0
+8 wlan0 0x0 10011 0 35777 53 5718 57 0 0 0 0 35777 53 0 0 0 0 5718 57
+9 wlan0 0x0 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+10 wlan0 0x0 10014 0 0 0 1098 13 0 0 0 0 0 0 0 0 0 0 1098 13
+11 wlan0 0x0 10014 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+12 wlan0 0x0 10021 0 562386 573 49228 549 0 0 0 0 562386 573 0 0 0 0 49228 549
+13 wlan0 0x0 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+14 wlan0 0x0 10031 0 3425 5 586 6 0 0 0 0 3425 5 0 0 0 0 586 6
+15 wlan0 0x0 10031 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+16 wlan0 0x7fffff0100000000 10021 0 562386 573 49228 549 0 0 0 0 562386 573 0 0 0 0 49228 549
+17 wlan0 0x7fffff0100000000 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+18 wlan0 0x7fffff0100000000 10031 0 3425 5 586 6 0 0 0 0 3425 5 0 0 0 0 586 6
+19 wlan0 0x7fffff0100000000 10031 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+20 rmnet2 0x0 0 0 547 5 118 2 40 1 243 1 264 3 0 0 62 1 56 1
+21 rmnet2 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+22 rmnet2 0x0 10001 0 1125899906842624 5 984 11 632 5 0 0 0 0 984 11 0 0 0 0
+23 rmnet2 0x0 10001 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+24 rmnet1 0x0 0 0 26736 174 7098 130 7210 97 18382 64 1144 13 2932 64 4054 64 112 2
+25 rmnet1 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+26 rmnet1 0x0 1000 0 75774 77 18038 78 75335 72 439 5 0 0 17668 73 370 5 0 0
+27 rmnet1 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+28 rmnet1 0x0 10007 0 269945 578 111632 586 269945 578 0 0 0 0 111632 586 0 0 0 0
+29 rmnet1 0x0 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+30 rmnet1 0x0 10011 0 1741256 6918 769778 7019 1741256 6918 0 0 0 0 769778 7019 0 0 0 0
+31 rmnet1 0x0 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+32 rmnet1 0x0 10014 0 0 0 786 12 0 0 0 0 0 0 786 12 0 0 0 0
+33 rmnet1 0x0 10014 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+34 rmnet1 0x0 10021 0 433533 1454 393420 1604 433533 1454 0 0 0 0 393420 1604 0 0 0 0
+35 rmnet1 0x0 10021 1 21215 33 10278 33 21215 33 0 0 0 0 10278 33 0 0 0 0
+36 rmnet1 0x0 10036 0 6310 25 3284 29 6310 25 0 0 0 0 3284 29 0 0 0 0
+37 rmnet1 0x0 10036 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+38 rmnet1 0x0 10047 0 34264 47 3936 34 34264 47 0 0 0 0 3936 34 0 0 0 0
+39 rmnet1 0x0 10047 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+40 rmnet1 0x4e7700000000 10011 0 9187 27 4248 33 9187 27 0 0 0 0 4248 33 0 0 0 0
+41 rmnet1 0x4e7700000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+42 rmnet1 0x1000000000000000 10007 0 2109 4 791 4 2109 4 0 0 0 0 791 4 0 0 0 0
+43 rmnet1 0x1000000000000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+44 rmnet1 0x1000000400000000 10007 0 9811 22 6286 22 9811 22 0 0 0 0 6286 22 0 0 0 0
+45 rmnet1 0x1000000400000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+46 rmnet1 0x1010000000000000 10021 0 164833 426 135392 527 164833 426 0 0 0 0 135392 527 0 0 0 0
+47 rmnet1 0x1010000000000000 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+48 rmnet1 0x1144000400000000 10011 0 10112 18 3334 17 10112 18 0 0 0 0 3334 17 0 0 0 0
+49 rmnet1 0x1144000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+50 rmnet1 0x1244000400000000 10011 0 1300 3 848 2 1300 3 0 0 0 0 848 2 0 0 0 0
+51 rmnet1 0x1244000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+52 rmnet1 0x3000000000000000 10007 0 10389 14 1521 12 10389 14 0 0 0 0 1521 12 0 0 0 0
+53 rmnet1 0x3000000000000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+54 rmnet1 0x3000000400000000 10007 0 238070 380 93938 404 238070 380 0 0 0 0 93938 404 0 0 0 0
+55 rmnet1 0x3000000400000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+56 rmnet1 0x3010000000000000 10021 0 219110 578 227423 676 219110 578 0 0 0 0 227423 676 0 0 0 0
+57 rmnet1 0x3010000000000000 10021 1 742 3 1265 3 742 3 0 0 0 0 1265 3 0 0 0 0
+58 rmnet1 0x3020000000000000 10021 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+59 rmnet1 0x3020000000000000 10021 1 20473 30 9013 30 20473 30 0 0 0 0 9013 30 0 0 0 0
+60 rmnet1 0x3144000400000000 10011 0 43963 92 34414 116 43963 92 0 0 0 0 34414 116 0 0 0 0
+61 rmnet1 0x3144000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+62 rmnet1 0x3244000400000000 10011 0 3486 8 1520 9 3486 8 0 0 0 0 1520 9 0 0 0 0
+63 rmnet1 0x3244000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+64 rmnet1 0x7fffff0100000000 10021 0 29102 56 8865 60 29102 56 0 0 0 0 8865 60 0 0 0 0
+65 rmnet1 0x7fffff0100000000 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+66 rmnet1 0x7fffff0300000000 1000 0 995 13 14145 14 995 13 0 0 0 0 14145 14 0 0 0 0
+67 rmnet1 0x7fffff0300000000 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+68 rmnet0 0x0 0 0 4312 49 1288 23 0 0 0 0 4312 49 0 0 0 0 1288 23
+69 rmnet0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+70 rmnet0 0x0 10080 0 22266 30 20976 30 0 0 0 0 22266 30 0 0 0 0 20976 30
+71 rmnet0 0x0 10080 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_incorrect_iface b/tests/unit/res/raw/xt_qtaguid_vpn_incorrect_iface
new file mode 100644
index 0000000..fc92715
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_vpn_incorrect_iface
@@ -0,0 +1,3 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 test_nss_tun0 0x0 1001 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0
+3 test1 0x0 1004 0 1100 100 1100 100 0 0 0 0 0 0 0 0 0 0 0 0
\ No newline at end of file
diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying
new file mode 100644
index 0000000..1ef1889
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying
@@ -0,0 +1,5 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0
+3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0
+4 test0 0x0 1004 0 3300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+5 test0 0x0 1004 1 0 0 1650 150 0 0 0 0 0 0 0 0 0 0 0 0
\ No newline at end of file
diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_compression b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_compression
new file mode 100644
index 0000000..6d6bf55
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_compression
@@ -0,0 +1,4 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 test_nss_tun0 0x0 1001 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0
+3 test_nss_tun0 0x0 1002 0 3000 300 3000 300 0 0 0 0 0 0 0 0 0 0 0 0
+4 test0 0x0 1004 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0
\ No newline at end of file
diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_own_traffic b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_own_traffic
new file mode 100644
index 0000000..2c2e5d2
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_own_traffic
@@ -0,0 +1,6 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0
+3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0
+4 test_nss_tun0 0x0 1004 0 5000 500 6000 600 0 0 0 0 0 0 0 0 0 0 0 0
+5 test0 0x0 1004 0 8800 800 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+6 test0 0x0 1004 1 0 0 8250 750 0 0 0 0 0 0 0 0 0 0 0 0
\ No newline at end of file
diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_two_vpn b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_two_vpn
new file mode 100644
index 0000000..eb0513b
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_two_vpn
@@ -0,0 +1,9 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0
+3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0
+4 test_nss_tun1 0x0 1001 0 3000 300 700 70 0 0 0 0 0 0 0 0 0 0 0 0
+5 test_nss_tun1 0x0 1002 0 500 50 250 25 0 0 0 0 0 0 0 0 0 0 0 0
+6 test0 0x0 1004 0 3300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+7 test0 0x0 1004 1 0 0 1650 150 0 0 0 0 0 0 0 0 0 0 0 0
+8 test1 0x0 1004 0 3850 350 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+9 test1 0x0 1004 1 0 0 1045 95 0 0 0 0 0 0 0 0 0 0 0 0
\ No newline at end of file
diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_rewrite_through_self b/tests/unit/res/raw/xt_qtaguid_vpn_rewrite_through_self
new file mode 100644
index 0000000..afcdd71
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_vpn_rewrite_through_self
@@ -0,0 +1,6 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0
+3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0
+4 test_nss_tun0 0x0 1004 0 0 0 1600 160 0 0 0 0 0 0 0 0 0 0 0 0
+5 test0 0x0 1004 1 0 0 1760 176 0 0 0 0 0 0 0 0 0 0 0 0
+6 test0 0x0 1004 0 3300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0
\ No newline at end of file
diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_duplication b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_duplication
new file mode 100644
index 0000000..d7c7eb9
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_duplication
@@ -0,0 +1,5 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 test_nss_tun0 0x0 1001 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0
+3 test_nss_tun0 0x0 1002 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0
+4 test0 0x0 1004 0 2200 200 2200 200 0 0 0 0 0 0 0 0 0 0 0 0
+5 test1 0x0 1004 0 2200 200 2200 200 0 0 0 0 0 0 0 0 0 0 0 0
\ No newline at end of file
diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split
new file mode 100644
index 0000000..38a3dce
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split
@@ -0,0 +1,4 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 test_nss_tun0 0x0 1001 0 500 50 1000 100 0 0 0 0 0 0 0 0 0 0 0 0
+3 test0 0x0 1004 0 330 30 660 60 0 0 0 0 0 0 0 0 0 0 0 0
+4 test1 0x0 1004 0 220 20 440 40 0 0 0 0 0 0 0 0 0 0 0 0
\ No newline at end of file
diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split_compression b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split_compression
new file mode 100644
index 0000000..d35244b
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split_compression
@@ -0,0 +1,4 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 test_nss_tun0 0x0 1001 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0
+3 test0 0x0 1004 0 600 60 600 60 0 0 0 0 0 0 0 0 0 0 0 0
+4 test1 0x0 1004 0 200 20 200 20 0 0 0 0 0 0 0 0 0 0 0 0
\ No newline at end of file
diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_with_clat b/tests/unit/res/raw/xt_qtaguid_vpn_with_clat
new file mode 100644
index 0000000..0d893d5
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_vpn_with_clat
@@ -0,0 +1,8 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0
+3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0
+4 v4-test0 0x0 1004 0 3300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+5 v4-test0 0x0 1004 1 0 0 1650 150 0 0 0 0 0 0 0 0 0 0 0 0
+6 test0 0x0 0 0 9300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+7 test0 0x0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+8 test0 0x0 1029 0 0 0 4650 150 0 0 0 0 0 0 0 0 0 0 0 0
\ No newline at end of file
diff --git a/tests/unit/res/raw/xt_qtaguid_with_clat b/tests/unit/res/raw/xt_qtaguid_with_clat
new file mode 100644
index 0000000..f04b32f
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_with_clat
@@ -0,0 +1,43 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 v4-wlan0 0x0 0 0 256 5 196 4 256 5 0 0 0 0 196 4 0 0 0 0
+3 v4-wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+4 v4-wlan0 0x0 1000 0 30312 25 1770 27 30236 24 76 1 0 0 1694 26 76 1 0 0
+5 v4-wlan0 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+6 v4-wlan0 0x0 10060 0 9398432 6717 169412 4235 9398432 6717 0 0 0 0 169412 4235 0 0 0 0
+7 v4-wlan0 0x0 10060 1 1448660 1041 31192 753 1448660 1041 0 0 0 0 31192 753 0 0 0 0
+8 v4-wlan0 0x0 10102 0 9702 16 2870 23 9702 16 0 0 0 0 2870 23 0 0 0 0
+9 v4-wlan0 0x0 10102 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+10 wlan0 0x0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+11 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+12 wlan0 0x0 1000 0 6126 13 2013 16 5934 11 192 2 0 0 1821 14 192 2 0 0
+13 wlan0 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+14 wlan0 0x0 10013 0 0 0 144 2 0 0 0 0 0 0 144 2 0 0 0 0
+15 wlan0 0x0 10013 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+16 wlan0 0x0 10018 0 5980263 4715 167667 1922 5972583 4709 0 0 7680 6 167667 1922 0 0 0 0
+17 wlan0 0x0 10018 1 43995 37 2766 27 43995 37 0 0 0 0 2766 27 0 0 0 0
+18 wlan0 0x0 10060 0 134356 133 8705 74 134356 133 0 0 0 0 8705 74 0 0 0 0
+19 wlan0 0x0 10060 1 294709 326 26448 256 294709 326 0 0 0 0 26448 256 0 0 0 0
+20 wlan0 0x0 10079 0 10926 13 1507 13 10926 13 0 0 0 0 1507 13 0 0 0 0
+21 wlan0 0x0 10079 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+22 wlan0 0x0 10102 0 25038 42 8245 57 25038 42 0 0 0 0 8245 57 0 0 0 0
+23 wlan0 0x0 10102 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+24 wlan0 0x0 10103 0 0 0 192 2 0 0 0 0 0 0 0 0 192 2 0 0
+25 wlan0 0x0 10103 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+26 wlan0 0x1000040700000000 10018 0 831 6 655 5 831 6 0 0 0 0 655 5 0 0 0 0
+27 wlan0 0x1000040700000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+28 wlan0 0x1000040b00000000 10018 0 1714 8 1561 7 1714 8 0 0 0 0 1561 7 0 0 0 0
+29 wlan0 0x1000040b00000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+30 wlan0 0x1000120300000000 10018 0 8243 11 2234 12 8243 11 0 0 0 0 2234 12 0 0 0 0
+31 wlan0 0x1000120300000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+32 wlan0 0x1000180300000000 10018 0 56368 49 4790 39 56368 49 0 0 0 0 4790 39 0 0 0 0
+33 wlan0 0x1000180300000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+34 wlan0 0x1000300000000000 10018 0 9488 17 18813 25 1808 11 0 0 7680 6 18813 25 0 0 0 0
+35 wlan0 0x1000300000000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+36 wlan0 0x3000180400000000 10018 0 131262 103 7416 103 131262 103 0 0 0 0 7416 103 0 0 0 0
+37 wlan0 0x3000180400000000 10018 1 43995 37 2766 27 43995 37 0 0 0 0 2766 27 0 0 0 0
+38 wlan0 0xffffff0100000000 10018 0 5771986 4518 131190 1725 5771986 4518 0 0 0 0 131190 1725 0 0 0 0
+39 wlan0 0xffffff0100000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+40 dummy0 0x0 0 0 0 0 168 3 0 0 0 0 0 0 0 0 0 0 168 3
+41 dummy0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+42 lo 0x0 0 0 1288 16 1288 16 0 0 532 8 756 8 0 0 532 8 756 8
+43 lo 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
diff --git a/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_after b/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_after
new file mode 100644
index 0000000..12d98ca
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_after
@@ -0,0 +1,189 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 r_rmnet_data0 0x0 0 0 0 0 392 6 0 0 0 0 0 0 0 0 0 0 392 6
+3 r_rmnet_data0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+4 v4-wlan0 0x0 0 0 58952 2072 2888 65 264 6 0 0 58688 2066 132 3 0 0 2756 62
+5 v4-wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+6 v4-wlan0 0x0 10034 0 6192 11 1445 11 6192 11 0 0 0 0 1445 11 0 0 0 0
+7 v4-wlan0 0x0 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+8 v4-wlan0 0x0 10057 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+9 v4-wlan0 0x0 10057 1 728 7 392 7 0 0 728 7 0 0 0 0 392 7 0 0
+10 v4-wlan0 0x0 10106 0 2232 18 2232 18 0 0 2232 18 0 0 0 0 2232 18 0 0
+11 v4-wlan0 0x0 10106 1 432952718 314238 5442288 121260 432950238 314218 2480 20 0 0 5433900 121029 8388 231 0 0
+12 wlan0 0x0 0 0 330187296 250652 0 0 329106990 236273 226202 1255 854104 13124 0 0 0 0 0 0
+13 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+14 wlan0 0x0 1000 0 77113 272 56151 575 77113 272 0 0 0 0 19191 190 36960 385 0 0
+15 wlan0 0x0 1000 1 20227 80 8356 72 18539 74 1688 6 0 0 7562 66 794 6 0 0
+16 wlan0 0x0 10006 0 80755 92 9122 99 80755 92 0 0 0 0 9122 99 0 0 0 0
+17 wlan0 0x0 10006 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+18 wlan0 0x0 10015 0 4390 7 14824 252 4390 7 0 0 0 0 14824 252 0 0 0 0
+19 wlan0 0x0 10015 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+20 wlan0 0x0 10018 0 4928 11 1741 14 4928 11 0 0 0 0 1741 14 0 0 0 0
+21 wlan0 0x0 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+22 wlan0 0x0 10020 0 21163552 34395 2351650 15326 21162947 34390 605 5 0 0 2351045 15321 605 5 0 0
+23 wlan0 0x0 10020 1 13835740 12938 1548795 6365 13833754 12920 1986 18 0 0 1546809 6347 1986 18 0 0
+24 wlan0 0x0 10023 0 13405 40 5042 44 13405 40 0 0 0 0 5042 44 0 0 0 0
+25 wlan0 0x0 10023 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+26 wlan0 0x0 10034 0 436394741 342648 6237981 80442 436394741 342648 0 0 0 0 6237981 80442 0 0 0 0
+27 wlan0 0x0 10034 1 64860872 51297 1335539 15546 64860872 51297 0 0 0 0 1335539 15546 0 0 0 0
+28 wlan0 0x0 10044 0 17614444 14774 521004 5694 17329882 14432 284562 342 0 0 419974 5408 101030 286 0 0
+29 wlan0 0x0 10044 1 17701 33 3100 28 17701 33 0 0 0 0 3100 28 0 0 0 0
+30 wlan0 0x0 10057 0 12312074 9339 436098 5450 12248060 9263 64014 76 0 0 414224 5388 21874 62 0 0
+31 wlan0 0x0 10057 1 1332953195 954797 31849632 457698 1331933207 953569 1019988 1228 0 0 31702284 456899 147348 799 0 0
+32 wlan0 0x0 10060 0 32972 200 433705 380 32972 200 0 0 0 0 433705 380 0 0 0 0
+33 wlan0 0x0 10060 1 32106 66 37789 87 32106 66 0 0 0 0 37789 87 0 0 0 0
+34 wlan0 0x0 10061 0 7675 23 2509 22 7675 23 0 0 0 0 2509 22 0 0 0 0
+35 wlan0 0x0 10061 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+36 wlan0 0x0 10074 0 38355 82 10447 97 38355 82 0 0 0 0 10447 97 0 0 0 0
+37 wlan0 0x0 10074 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+38 wlan0 0x0 10078 0 49013 79 7167 69 49013 79 0 0 0 0 7167 69 0 0 0 0
+39 wlan0 0x0 10078 1 5872 8 1236 10 5872 8 0 0 0 0 1236 10 0 0 0 0
+40 wlan0 0x0 10082 0 8301 13 1981 15 8301 13 0 0 0 0 1981 15 0 0 0 0
+41 wlan0 0x0 10082 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+42 wlan0 0x0 10086 0 7001 14 1579 15 7001 14 0 0 0 0 1579 15 0 0 0 0
+43 wlan0 0x0 10086 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+44 wlan0 0x0 10090 0 24327795 20224 920502 14661 24327795 20224 0 0 0 0 920502 14661 0 0 0 0
+45 wlan0 0x0 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+46 wlan0 0x0 10092 0 36849 78 12449 81 36849 78 0 0 0 0 12449 81 0 0 0 0
+47 wlan0 0x0 10092 1 60 1 103 1 60 1 0 0 0 0 103 1 0 0 0 0
+48 wlan0 0x0 10095 0 131962 223 37069 241 131962 223 0 0 0 0 37069 241 0 0 0 0
+49 wlan0 0x0 10095 1 12949 21 3930 21 12949 21 0 0 0 0 3930 21 0 0 0 0
+50 wlan0 0x0 10106 0 30899554 22679 632476 12296 30895334 22645 4220 34 0 0 628256 12262 4220 34 0 0
+51 wlan0 0x0 10106 1 88923475 64963 1606962 35612 88917201 64886 3586 29 2688 48 1602032 35535 4930 77 0 0
+52 wlan0 0x40700000000 10020 0 705732 10589 404428 5504 705732 10589 0 0 0 0 404428 5504 0 0 0 0
+53 wlan0 0x40700000000 10020 1 2376 36 1296 18 2376 36 0 0 0 0 1296 18 0 0 0 0
+54 wlan0 0x40800000000 10020 0 34624 146 122525 160 34624 146 0 0 0 0 122525 160 0 0 0 0
+55 wlan0 0x40800000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+56 wlan0 0x40b00000000 10020 0 22411 85 7364 57 22411 85 0 0 0 0 7364 57 0 0 0 0
+57 wlan0 0x40b00000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+58 wlan0 0x120300000000 10020 0 76641 241 32783 169 76641 241 0 0 0 0 32783 169 0 0 0 0
+59 wlan0 0x120300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+60 wlan0 0x130100000000 10020 0 73101 287 23236 203 73101 287 0 0 0 0 23236 203 0 0 0 0
+61 wlan0 0x130100000000 10020 1 264 4 144 2 264 4 0 0 0 0 144 2 0 0 0 0
+62 wlan0 0x180300000000 10020 0 330648 399 24736 232 330648 399 0 0 0 0 24736 232 0 0 0 0
+63 wlan0 0x180300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+64 wlan0 0x180400000000 10020 0 21865 59 5022 42 21865 59 0 0 0 0 5022 42 0 0 0 0
+65 wlan0 0x180400000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+66 wlan0 0x300000000000 10020 0 15984 65 26927 57 15984 65 0 0 0 0 26927 57 0 0 0 0
+67 wlan0 0x300000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+68 wlan0 0x1065fff00000000 10020 0 131871 599 93783 445 131871 599 0 0 0 0 93783 445 0 0 0 0
+69 wlan0 0x1065fff00000000 10020 1 264 4 144 2 264 4 0 0 0 0 144 2 0 0 0 0
+70 wlan0 0x1b24f4600000000 10034 0 15445 42 23329 45 15445 42 0 0 0 0 23329 45 0 0 0 0
+71 wlan0 0x1b24f4600000000 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+72 wlan0 0x1000010000000000 10020 0 5542 9 1364 10 5542 9 0 0 0 0 1364 10 0 0 0 0
+73 wlan0 0x1000010000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+74 wlan0 0x1000040100000000 10020 0 47196 184 213319 257 47196 184 0 0 0 0 213319 257 0 0 0 0
+75 wlan0 0x1000040100000000 10020 1 60 1 103 1 60 1 0 0 0 0 103 1 0 0 0 0
+76 wlan0 0x1000040700000000 10020 0 11599 50 10786 47 11599 50 0 0 0 0 10786 47 0 0 0 0
+77 wlan0 0x1000040700000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+78 wlan0 0x1000040800000000 10020 0 21902 145 174139 166 21902 145 0 0 0 0 174139 166 0 0 0 0
+79 wlan0 0x1000040800000000 10020 1 8568 88 105743 90 8568 88 0 0 0 0 105743 90 0 0 0 0
+80 wlan0 0x1000100300000000 10020 0 55213 118 194551 199 55213 118 0 0 0 0 194551 199 0 0 0 0
+81 wlan0 0x1000100300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+82 wlan0 0x1000120300000000 10020 0 50826 74 21153 70 50826 74 0 0 0 0 21153 70 0 0 0 0
+83 wlan0 0x1000120300000000 10020 1 72 1 175 2 72 1 0 0 0 0 175 2 0 0 0 0
+84 wlan0 0x1000180300000000 10020 0 744198 657 65437 592 744198 657 0 0 0 0 65437 592 0 0 0 0
+85 wlan0 0x1000180300000000 10020 1 144719 132 10989 108 144719 132 0 0 0 0 10989 108 0 0 0 0
+86 wlan0 0x1000180600000000 10020 0 4599 8 1928 10 4599 8 0 0 0 0 1928 10 0 0 0 0
+87 wlan0 0x1000180600000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+88 wlan0 0x1000250000000000 10020 0 57740 98 13076 88 57740 98 0 0 0 0 13076 88 0 0 0 0
+89 wlan0 0x1000250000000000 10020 1 328 3 414 4 207 2 121 1 0 0 293 3 121 1 0 0
+90 wlan0 0x1000300000000000 10020 0 7675 30 31331 32 7675 30 0 0 0 0 31331 32 0 0 0 0
+91 wlan0 0x1000300000000000 10020 1 30173 97 101335 100 30173 97 0 0 0 0 101335 100 0 0 0 0
+92 wlan0 0x1000310200000000 10020 0 1681 9 2194 9 1681 9 0 0 0 0 2194 9 0 0 0 0
+93 wlan0 0x1000310200000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+94 wlan0 0x1000360000000000 10020 0 5606 20 2831 20 5606 20 0 0 0 0 2831 20 0 0 0 0
+95 wlan0 0x1000360000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+96 wlan0 0x11065fff00000000 10020 0 18363 91 83367 104 18363 91 0 0 0 0 83367 104 0 0 0 0
+97 wlan0 0x11065fff00000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+98 wlan0 0x3000009600000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+99 wlan0 0x3000009600000000 10020 1 6163 18 2424 18 6163 18 0 0 0 0 2424 18 0 0 0 0
+100 wlan0 0x3000009800000000 10020 0 23337 46 8723 39 23337 46 0 0 0 0 8723 39 0 0 0 0
+101 wlan0 0x3000009800000000 10020 1 33744 93 72437 89 33744 93 0 0 0 0 72437 89 0 0 0 0
+102 wlan0 0x3000020000000000 10020 0 4124 11 8969 19 4124 11 0 0 0 0 8969 19 0 0 0 0
+103 wlan0 0x3000020000000000 10020 1 5993 11 3815 14 5993 11 0 0 0 0 3815 14 0 0 0 0
+104 wlan0 0x3000040100000000 10020 0 113809 342 135666 308 113809 342 0 0 0 0 135666 308 0 0 0 0
+105 wlan0 0x3000040100000000 10020 1 142508 642 500579 637 142508 642 0 0 0 0 500579 637 0 0 0 0
+106 wlan0 0x3000040700000000 10020 0 365815 5119 213340 2733 365815 5119 0 0 0 0 213340 2733 0 0 0 0
+107 wlan0 0x3000040700000000 10020 1 30747 130 18408 100 30747 130 0 0 0 0 18408 100 0 0 0 0
+108 wlan0 0x3000040800000000 10020 0 34672 112 68623 92 34672 112 0 0 0 0 68623 92 0 0 0 0
+109 wlan0 0x3000040800000000 10020 1 78443 199 140944 192 78443 199 0 0 0 0 140944 192 0 0 0 0
+110 wlan0 0x3000040b00000000 10020 0 14949 33 4017 26 14949 33 0 0 0 0 4017 26 0 0 0 0
+111 wlan0 0x3000040b00000000 10020 1 996 15 576 8 996 15 0 0 0 0 576 8 0 0 0 0
+112 wlan0 0x3000090000000000 10020 0 11826 67 7309 52 11826 67 0 0 0 0 7309 52 0 0 0 0
+113 wlan0 0x3000090000000000 10020 1 24805 41 4785 41 24805 41 0 0 0 0 4785 41 0 0 0 0
+114 wlan0 0x3000100300000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+115 wlan0 0x3000100300000000 10020 1 3112 10 1628 10 3112 10 0 0 0 0 1628 10 0 0 0 0
+116 wlan0 0x3000120300000000 10020 0 38249 107 20374 85 38249 107 0 0 0 0 20374 85 0 0 0 0
+117 wlan0 0x3000120300000000 10020 1 122581 174 36792 143 122581 174 0 0 0 0 36792 143 0 0 0 0
+118 wlan0 0x3000130100000000 10020 0 2700 41 1524 21 2700 41 0 0 0 0 1524 21 0 0 0 0
+119 wlan0 0x3000130100000000 10020 1 22515 59 8366 52 22515 59 0 0 0 0 8366 52 0 0 0 0
+120 wlan0 0x3000180200000000 10020 0 6411 18 14511 20 6411 18 0 0 0 0 14511 20 0 0 0 0
+121 wlan0 0x3000180200000000 10020 1 336 5 319 4 336 5 0 0 0 0 319 4 0 0 0 0
+122 wlan0 0x3000180300000000 10020 0 129301 136 17622 97 129301 136 0 0 0 0 17622 97 0 0 0 0
+123 wlan0 0x3000180300000000 10020 1 464787 429 41703 336 464787 429 0 0 0 0 41703 336 0 0 0 0
+124 wlan0 0x3000180400000000 10020 0 11014 39 2787 25 11014 39 0 0 0 0 2787 25 0 0 0 0
+125 wlan0 0x3000180400000000 10020 1 144040 139 7540 80 144040 139 0 0 0 0 7540 80 0 0 0 0
+126 wlan0 0x3000210100000000 10020 0 10278 44 4579 33 10278 44 0 0 0 0 4579 33 0 0 0 0
+127 wlan0 0x3000210100000000 10020 1 31151 73 14159 47 31151 73 0 0 0 0 14159 47 0 0 0 0
+128 wlan0 0x3000250000000000 10020 0 132 2 72 1 132 2 0 0 0 0 72 1 0 0 0 0
+129 wlan0 0x3000250000000000 10020 1 76614 143 17711 130 76080 137 534 6 0 0 17177 124 534 6 0 0
+130 wlan0 0x3000260100000000 10020 0 9426 26 3535 20 9426 26 0 0 0 0 3535 20 0 0 0 0
+131 wlan0 0x3000260100000000 10020 1 468 7 288 4 468 7 0 0 0 0 288 4 0 0 0 0
+132 wlan0 0x3000300000000000 10020 0 7241 29 12055 26 7241 29 0 0 0 0 12055 26 0 0 0 0
+133 wlan0 0x3000300000000000 10020 1 3273 23 11232 21 3273 23 0 0 0 0 11232 21 0 0 0 0
+134 wlan0 0x3000310000000000 10020 0 132 2 72 1 132 2 0 0 0 0 72 1 0 0 0 0
+135 wlan0 0x3000310000000000 10020 1 53425 64 8721 62 53425 64 0 0 0 0 8721 62 0 0 0 0
+136 wlan0 0x3000310500000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+137 wlan0 0x3000310500000000 10020 1 9929 16 3879 18 9929 16 0 0 0 0 3879 18 0 0 0 0
+138 wlan0 0x3000320100000000 10020 0 6844 14 3745 13 6844 14 0 0 0 0 3745 13 0 0 0 0
+139 wlan0 0x3000320100000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+140 wlan0 0x3000360000000000 10020 0 8855 43 4749 31 8855 43 0 0 0 0 4749 31 0 0 0 0
+141 wlan0 0x3000360000000000 10020 1 5597 19 2456 19 5597 19 0 0 0 0 2456 19 0 0 0 0
+142 wlan0 0x3010000000000000 10090 0 605140 527 38435 429 605140 527 0 0 0 0 38435 429 0 0 0 0
+143 wlan0 0x3010000000000000 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+144 wlan0 0x31065fff00000000 10020 0 22011 67 29665 64 22011 67 0 0 0 0 29665 64 0 0 0 0
+145 wlan0 0x31065fff00000000 10020 1 10695 34 18347 35 10695 34 0 0 0 0 18347 35 0 0 0 0
+146 wlan0 0x32e544f900000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+147 wlan0 0x32e544f900000000 10034 1 40143 54 7299 61 40143 54 0 0 0 0 7299 61 0 0 0 0
+148 wlan0 0x58872a4400000000 10018 0 4928 11 1669 13 4928 11 0 0 0 0 1669 13 0 0 0 0
+149 wlan0 0x58872a4400000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+150 wlan0 0x5caeaa7b00000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+151 wlan0 0x5caeaa7b00000000 10034 1 74971 73 7103 75 74971 73 0 0 0 0 7103 75 0 0 0 0
+152 wlan0 0x9e00923800000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+153 wlan0 0x9e00923800000000 10034 1 72385 98 13072 110 72385 98 0 0 0 0 13072 110 0 0 0 0
+154 wlan0 0xb972bdd400000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+155 wlan0 0xb972bdd400000000 10034 1 15282 24 3034 27 15282 24 0 0 0 0 3034 27 0 0 0 0
+156 wlan0 0xc7c9f7ba00000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+157 wlan0 0xc7c9f7ba00000000 10034 1 194915 185 13316 138 194915 185 0 0 0 0 13316 138 0 0 0 0
+158 wlan0 0xc9395b2600000000 10034 0 6991 13 6215 14 6991 13 0 0 0 0 6215 14 0 0 0 0
+159 wlan0 0xc9395b2600000000 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+160 wlan0 0xdaddf21100000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+161 wlan0 0xdaddf21100000000 10034 1 928676 849 81570 799 928676 849 0 0 0 0 81570 799 0 0 0 0
+162 wlan0 0xe8d195d100000000 10020 0 516 8 288 4 516 8 0 0 0 0 288 4 0 0 0 0
+163 wlan0 0xe8d195d100000000 10020 1 5905 15 2622 15 5905 15 0 0 0 0 2622 15 0 0 0 0
+164 wlan0 0xe8d195d100000000 10034 0 236640 524 312523 555 236640 524 0 0 0 0 312523 555 0 0 0 0
+165 wlan0 0xe8d195d100000000 10034 1 319028 539 188776 553 319028 539 0 0 0 0 188776 553 0 0 0 0
+166 wlan0 0xffffff0100000000 10006 0 80755 92 9122 99 80755 92 0 0 0 0 9122 99 0 0 0 0
+167 wlan0 0xffffff0100000000 10006 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+168 wlan0 0xffffff0100000000 10020 0 17874405 14068 223987 3065 17874405 14068 0 0 0 0 223987 3065 0 0 0 0
+169 wlan0 0xffffff0100000000 10020 1 11011258 8672 177693 2407 11011258 8672 0 0 0 0 177693 2407 0 0 0 0
+170 wlan0 0xffffff0100000000 10034 0 436062595 341880 5843990 79630 436062595 341880 0 0 0 0 5843990 79630 0 0 0 0
+171 wlan0 0xffffff0100000000 10034 1 63201220 49447 1005882 13713 63201220 49447 0 0 0 0 1005882 13713 0 0 0 0
+172 wlan0 0xffffff0100000000 10044 0 17159287 13702 356212 4778 17159287 13702 0 0 0 0 356212 4778 0 0 0 0
+173 wlan0 0xffffff0100000000 10044 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+174 wlan0 0xffffff0100000000 10078 0 10439 17 1665 15 10439 17 0 0 0 0 1665 15 0 0 0 0
+175 wlan0 0xffffff0100000000 10078 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+176 wlan0 0xffffff0100000000 10090 0 23722655 19697 881995 14231 23722655 19697 0 0 0 0 881995 14231 0 0 0 0
+177 wlan0 0xffffff0100000000 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+178 wlan0 0xffffff0500000000 1000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+179 wlan0 0xffffff0500000000 1000 1 1592 5 314 1 0 0 1592 5 0 0 0 0 314 1 0 0
+180 wlan0 0xffffff0600000000 1000 0 0 0 36960 385 0 0 0 0 0 0 0 0 36960 385 0 0
+181 wlan0 0xffffff0600000000 1000 1 96 1 480 5 0 0 96 1 0 0 0 0 480 5 0 0
+182 wlan0 0xffffff0700000000 1000 0 38732 229 16567 163 38732 229 0 0 0 0 16567 163 0 0 0 0
+183 wlan0 0xffffff0700000000 1000 1 18539 74 7562 66 18539 74 0 0 0 0 7562 66 0 0 0 0
+184 wlan0 0xffffff0900000000 1000 0 38381 43 2624 27 38381 43 0 0 0 0 2624 27 0 0 0 0
+185 wlan0 0xffffff0900000000 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+186 dummy0 0x0 0 0 0 0 168 3 0 0 0 0 0 0 0 0 0 0 168 3
+187 dummy0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+188 wlan0 0x0 1029 0 0 0 8524052 130894 0 0 0 0 0 0 7871216 121284 108568 1325 544268 8285
+189 wlan0 0x0 1029 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
diff --git a/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_before b/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_before
new file mode 100644
index 0000000..ce4bcc3
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_before
@@ -0,0 +1,187 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 r_rmnet_data0 0x0 0 0 0 0 392 6 0 0 0 0 0 0 0 0 0 0 392 6
+3 r_rmnet_data0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+4 v4-wlan0 0x0 0 0 58848 2070 2836 64 160 4 0 0 58688 2066 80 2 0 0 2756 62
+5 v4-wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+6 v4-wlan0 0x0 10034 0 6192 11 1445 11 6192 11 0 0 0 0 1445 11 0 0 0 0
+7 v4-wlan0 0x0 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+8 v4-wlan0 0x0 10057 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+9 v4-wlan0 0x0 10057 1 728 7 392 7 0 0 728 7 0 0 0 0 392 7 0 0
+10 v4-wlan0 0x0 10106 0 1488 12 1488 12 0 0 1488 12 0 0 0 0 1488 12 0 0
+11 v4-wlan0 0x0 10106 1 323981189 235142 3509032 84542 323979453 235128 1736 14 0 0 3502676 84363 6356 179 0 0
+12 wlan0 0x0 0 0 330187296 250652 0 0 329106990 236273 226202 1255 854104 13124 0 0 0 0 0 0
+13 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+14 wlan0 0x0 1000 0 77113 272 56151 575 77113 272 0 0 0 0 19191 190 36960 385 0 0
+15 wlan0 0x0 1000 1 20227 80 8356 72 18539 74 1688 6 0 0 7562 66 794 6 0 0
+16 wlan0 0x0 10006 0 80755 92 9122 99 80755 92 0 0 0 0 9122 99 0 0 0 0
+17 wlan0 0x0 10006 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+18 wlan0 0x0 10015 0 4390 7 14824 252 4390 7 0 0 0 0 14824 252 0 0 0 0
+19 wlan0 0x0 10015 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+20 wlan0 0x0 10018 0 4928 11 1741 14 4928 11 0 0 0 0 1741 14 0 0 0 0
+21 wlan0 0x0 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+22 wlan0 0x0 10020 0 21141412 34316 2329881 15262 21140807 34311 605 5 0 0 2329276 15257 605 5 0 0
+23 wlan0 0x0 10020 1 13835740 12938 1548555 6362 13833754 12920 1986 18 0 0 1546569 6344 1986 18 0 0
+24 wlan0 0x0 10023 0 13405 40 5042 44 13405 40 0 0 0 0 5042 44 0 0 0 0
+25 wlan0 0x0 10023 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+26 wlan0 0x0 10034 0 436394741 342648 6237981 80442 436394741 342648 0 0 0 0 6237981 80442 0 0 0 0
+27 wlan0 0x0 10034 1 64860872 51297 1335539 15546 64860872 51297 0 0 0 0 1335539 15546 0 0 0 0
+28 wlan0 0x0 10044 0 17614444 14774 521004 5694 17329882 14432 284562 342 0 0 419974 5408 101030 286 0 0
+29 wlan0 0x0 10044 1 17701 33 3100 28 17701 33 0 0 0 0 3100 28 0 0 0 0
+30 wlan0 0x0 10057 0 12311735 9335 435954 5448 12247721 9259 64014 76 0 0 414080 5386 21874 62 0 0
+31 wlan0 0x0 10057 1 1332953195 954797 31849632 457698 1331933207 953569 1019988 1228 0 0 31702284 456899 147348 799 0 0
+32 wlan0 0x0 10060 0 32972 200 433705 380 32972 200 0 0 0 0 433705 380 0 0 0 0
+33 wlan0 0x0 10060 1 32106 66 37789 87 32106 66 0 0 0 0 37789 87 0 0 0 0
+34 wlan0 0x0 10061 0 7675 23 2509 22 7675 23 0 0 0 0 2509 22 0 0 0 0
+35 wlan0 0x0 10061 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+36 wlan0 0x0 10074 0 38355 82 10447 97 38355 82 0 0 0 0 10447 97 0 0 0 0
+37 wlan0 0x0 10074 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+38 wlan0 0x0 10078 0 49013 79 7167 69 49013 79 0 0 0 0 7167 69 0 0 0 0
+39 wlan0 0x0 10078 1 5872 8 1236 10 5872 8 0 0 0 0 1236 10 0 0 0 0
+40 wlan0 0x0 10082 0 8301 13 1981 15 8301 13 0 0 0 0 1981 15 0 0 0 0
+41 wlan0 0x0 10082 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+42 wlan0 0x0 10086 0 7001 14 1579 15 7001 14 0 0 0 0 1579 15 0 0 0 0
+43 wlan0 0x0 10086 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+44 wlan0 0x0 10090 0 24327795 20224 920502 14661 24327795 20224 0 0 0 0 920502 14661 0 0 0 0
+45 wlan0 0x0 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+46 wlan0 0x0 10092 0 36849 78 12449 81 36849 78 0 0 0 0 12449 81 0 0 0 0
+47 wlan0 0x0 10092 1 60 1 103 1 60 1 0 0 0 0 103 1 0 0 0 0
+48 wlan0 0x0 10095 0 131962 223 37069 241 131962 223 0 0 0 0 37069 241 0 0 0 0
+49 wlan0 0x0 10095 1 12949 21 3930 21 12949 21 0 0 0 0 3930 21 0 0 0 0
+50 wlan0 0x0 10106 0 30899554 22679 632476 12296 30895334 22645 4220 34 0 0 628256 12262 4220 34 0 0
+51 wlan0 0x0 10106 1 88922349 64952 1605126 35599 88916075 64875 3586 29 2688 48 1600196 35522 4930 77 0 0
+52 wlan0 0x40700000000 10020 0 705732 10589 404428 5504 705732 10589 0 0 0 0 404428 5504 0 0 0 0
+53 wlan0 0x40700000000 10020 1 2376 36 1296 18 2376 36 0 0 0 0 1296 18 0 0 0 0
+54 wlan0 0x40800000000 10020 0 34624 146 122525 160 34624 146 0 0 0 0 122525 160 0 0 0 0
+55 wlan0 0x40800000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+56 wlan0 0x40b00000000 10020 0 22411 85 7364 57 22411 85 0 0 0 0 7364 57 0 0 0 0
+57 wlan0 0x40b00000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+58 wlan0 0x120300000000 10020 0 76641 241 32783 169 76641 241 0 0 0 0 32783 169 0 0 0 0
+59 wlan0 0x120300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+60 wlan0 0x130100000000 10020 0 73101 287 23236 203 73101 287 0 0 0 0 23236 203 0 0 0 0
+61 wlan0 0x130100000000 10020 1 264 4 144 2 264 4 0 0 0 0 144 2 0 0 0 0
+62 wlan0 0x180300000000 10020 0 330648 399 24736 232 330648 399 0 0 0 0 24736 232 0 0 0 0
+63 wlan0 0x180300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+64 wlan0 0x180400000000 10020 0 21865 59 5022 42 21865 59 0 0 0 0 5022 42 0 0 0 0
+65 wlan0 0x180400000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+66 wlan0 0x300000000000 10020 0 15984 65 26927 57 15984 65 0 0 0 0 26927 57 0 0 0 0
+67 wlan0 0x300000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+68 wlan0 0x1065fff00000000 10020 0 131871 599 93783 445 131871 599 0 0 0 0 93783 445 0 0 0 0
+69 wlan0 0x1065fff00000000 10020 1 264 4 144 2 264 4 0 0 0 0 144 2 0 0 0 0
+70 wlan0 0x1b24f4600000000 10034 0 15445 42 23329 45 15445 42 0 0 0 0 23329 45 0 0 0 0
+71 wlan0 0x1b24f4600000000 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+72 wlan0 0x1000010000000000 10020 0 5542 9 1364 10 5542 9 0 0 0 0 1364 10 0 0 0 0
+73 wlan0 0x1000010000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+74 wlan0 0x1000040100000000 10020 0 47196 184 213319 257 47196 184 0 0 0 0 213319 257 0 0 0 0
+75 wlan0 0x1000040100000000 10020 1 60 1 103 1 60 1 0 0 0 0 103 1 0 0 0 0
+76 wlan0 0x1000040700000000 10020 0 11599 50 10786 47 11599 50 0 0 0 0 10786 47 0 0 0 0
+77 wlan0 0x1000040700000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+78 wlan0 0x1000040800000000 10020 0 21902 145 174139 166 21902 145 0 0 0 0 174139 166 0 0 0 0
+79 wlan0 0x1000040800000000 10020 1 8568 88 105743 90 8568 88 0 0 0 0 105743 90 0 0 0 0
+80 wlan0 0x1000100300000000 10020 0 55213 118 194551 199 55213 118 0 0 0 0 194551 199 0 0 0 0
+81 wlan0 0x1000100300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+82 wlan0 0x1000120300000000 10020 0 50826 74 21153 70 50826 74 0 0 0 0 21153 70 0 0 0 0
+83 wlan0 0x1000120300000000 10020 1 72 1 175 2 72 1 0 0 0 0 175 2 0 0 0 0
+84 wlan0 0x1000180300000000 10020 0 744198 657 65437 592 744198 657 0 0 0 0 65437 592 0 0 0 0
+85 wlan0 0x1000180300000000 10020 1 144719 132 10989 108 144719 132 0 0 0 0 10989 108 0 0 0 0
+86 wlan0 0x1000180600000000 10020 0 4599 8 1928 10 4599 8 0 0 0 0 1928 10 0 0 0 0
+87 wlan0 0x1000180600000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+88 wlan0 0x1000250000000000 10020 0 57740 98 13076 88 57740 98 0 0 0 0 13076 88 0 0 0 0
+89 wlan0 0x1000250000000000 10020 1 328 3 414 4 207 2 121 1 0 0 293 3 121 1 0 0
+90 wlan0 0x1000300000000000 10020 0 7675 30 31331 32 7675 30 0 0 0 0 31331 32 0 0 0 0
+91 wlan0 0x1000300000000000 10020 1 30173 97 101335 100 30173 97 0 0 0 0 101335 100 0 0 0 0
+92 wlan0 0x1000310200000000 10020 0 1681 9 2194 9 1681 9 0 0 0 0 2194 9 0 0 0 0
+93 wlan0 0x1000310200000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+94 wlan0 0x1000360000000000 10020 0 5606 20 2831 20 5606 20 0 0 0 0 2831 20 0 0 0 0
+95 wlan0 0x1000360000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+96 wlan0 0x11065fff00000000 10020 0 18363 91 83367 104 18363 91 0 0 0 0 83367 104 0 0 0 0
+97 wlan0 0x11065fff00000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+98 wlan0 0x3000009600000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+99 wlan0 0x3000009600000000 10020 1 6163 18 2424 18 6163 18 0 0 0 0 2424 18 0 0 0 0
+100 wlan0 0x3000009800000000 10020 0 23337 46 8723 39 23337 46 0 0 0 0 8723 39 0 0 0 0
+101 wlan0 0x3000009800000000 10020 1 33744 93 72437 89 33744 93 0 0 0 0 72437 89 0 0 0 0
+102 wlan0 0x3000020000000000 10020 0 4124 11 8969 19 4124 11 0 0 0 0 8969 19 0 0 0 0
+103 wlan0 0x3000020000000000 10020 1 5993 11 3815 14 5993 11 0 0 0 0 3815 14 0 0 0 0
+104 wlan0 0x3000040100000000 10020 0 106718 322 121557 287 106718 322 0 0 0 0 121557 287 0 0 0 0
+105 wlan0 0x3000040100000000 10020 1 142508 642 500579 637 142508 642 0 0 0 0 500579 637 0 0 0 0
+106 wlan0 0x3000040700000000 10020 0 365419 5113 213124 2730 365419 5113 0 0 0 0 213124 2730 0 0 0 0
+107 wlan0 0x3000040700000000 10020 1 30747 130 18408 100 30747 130 0 0 0 0 18408 100 0 0 0 0
+108 wlan0 0x3000040800000000 10020 0 34672 112 68623 92 34672 112 0 0 0 0 68623 92 0 0 0 0
+109 wlan0 0x3000040800000000 10020 1 78443 199 140944 192 78443 199 0 0 0 0 140944 192 0 0 0 0
+110 wlan0 0x3000040b00000000 10020 0 14949 33 4017 26 14949 33 0 0 0 0 4017 26 0 0 0 0
+111 wlan0 0x3000040b00000000 10020 1 996 15 576 8 996 15 0 0 0 0 576 8 0 0 0 0
+112 wlan0 0x3000090000000000 10020 0 4017 28 3610 25 4017 28 0 0 0 0 3610 25 0 0 0 0
+113 wlan0 0x3000090000000000 10020 1 24805 41 4545 38 24805 41 0 0 0 0 4545 38 0 0 0 0
+114 wlan0 0x3000100300000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+115 wlan0 0x3000100300000000 10020 1 3112 10 1628 10 3112 10 0 0 0 0 1628 10 0 0 0 0
+116 wlan0 0x3000120300000000 10020 0 38249 107 20374 85 38249 107 0 0 0 0 20374 85 0 0 0 0
+117 wlan0 0x3000120300000000 10020 1 122581 174 36792 143 122581 174 0 0 0 0 36792 143 0 0 0 0
+118 wlan0 0x3000130100000000 10020 0 2700 41 1524 21 2700 41 0 0 0 0 1524 21 0 0 0 0
+119 wlan0 0x3000130100000000 10020 1 22515 59 8366 52 22515 59 0 0 0 0 8366 52 0 0 0 0
+120 wlan0 0x3000180200000000 10020 0 6411 18 14511 20 6411 18 0 0 0 0 14511 20 0 0 0 0
+121 wlan0 0x3000180200000000 10020 1 336 5 319 4 336 5 0 0 0 0 319 4 0 0 0 0
+122 wlan0 0x3000180300000000 10020 0 129301 136 17622 97 129301 136 0 0 0 0 17622 97 0 0 0 0
+123 wlan0 0x3000180300000000 10020 1 464787 429 41703 336 464787 429 0 0 0 0 41703 336 0 0 0 0
+124 wlan0 0x3000180400000000 10020 0 11014 39 2787 25 11014 39 0 0 0 0 2787 25 0 0 0 0
+125 wlan0 0x3000180400000000 10020 1 144040 139 7540 80 144040 139 0 0 0 0 7540 80 0 0 0 0
+126 wlan0 0x3000210100000000 10020 0 10278 44 4579 33 10278 44 0 0 0 0 4579 33 0 0 0 0
+127 wlan0 0x3000210100000000 10020 1 31151 73 14159 47 31151 73 0 0 0 0 14159 47 0 0 0 0
+128 wlan0 0x3000250000000000 10020 0 132 2 72 1 132 2 0 0 0 0 72 1 0 0 0 0
+129 wlan0 0x3000250000000000 10020 1 76614 143 17711 130 76080 137 534 6 0 0 17177 124 534 6 0 0
+130 wlan0 0x3000260100000000 10020 0 9426 26 3535 20 9426 26 0 0 0 0 3535 20 0 0 0 0
+131 wlan0 0x3000260100000000 10020 1 468 7 288 4 468 7 0 0 0 0 288 4 0 0 0 0
+132 wlan0 0x3000300000000000 10020 0 7241 29 12055 26 7241 29 0 0 0 0 12055 26 0 0 0 0
+133 wlan0 0x3000300000000000 10020 1 3273 23 11232 21 3273 23 0 0 0 0 11232 21 0 0 0 0
+134 wlan0 0x3000310000000000 10020 0 132 2 72 1 132 2 0 0 0 0 72 1 0 0 0 0
+135 wlan0 0x3000310000000000 10020 1 53425 64 8721 62 53425 64 0 0 0 0 8721 62 0 0 0 0
+136 wlan0 0x3000310500000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+137 wlan0 0x3000310500000000 10020 1 9929 16 3879 18 9929 16 0 0 0 0 3879 18 0 0 0 0
+138 wlan0 0x3000360000000000 10020 0 8855 43 4749 31 8855 43 0 0 0 0 4749 31 0 0 0 0
+139 wlan0 0x3000360000000000 10020 1 5597 19 2456 19 5597 19 0 0 0 0 2456 19 0 0 0 0
+140 wlan0 0x3010000000000000 10090 0 605140 527 38435 429 605140 527 0 0 0 0 38435 429 0 0 0 0
+141 wlan0 0x3010000000000000 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+142 wlan0 0x31065fff00000000 10020 0 22011 67 29665 64 22011 67 0 0 0 0 29665 64 0 0 0 0
+143 wlan0 0x31065fff00000000 10020 1 10695 34 18347 35 10695 34 0 0 0 0 18347 35 0 0 0 0
+144 wlan0 0x32e544f900000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+145 wlan0 0x32e544f900000000 10034 1 40143 54 7299 61 40143 54 0 0 0 0 7299 61 0 0 0 0
+146 wlan0 0x58872a4400000000 10018 0 4928 11 1669 13 4928 11 0 0 0 0 1669 13 0 0 0 0
+147 wlan0 0x58872a4400000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+148 wlan0 0x5caeaa7b00000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+149 wlan0 0x5caeaa7b00000000 10034 1 74971 73 7103 75 74971 73 0 0 0 0 7103 75 0 0 0 0
+150 wlan0 0x9e00923800000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+151 wlan0 0x9e00923800000000 10034 1 72385 98 13072 110 72385 98 0 0 0 0 13072 110 0 0 0 0
+152 wlan0 0xb972bdd400000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+153 wlan0 0xb972bdd400000000 10034 1 15282 24 3034 27 15282 24 0 0 0 0 3034 27 0 0 0 0
+154 wlan0 0xc7c9f7ba00000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+155 wlan0 0xc7c9f7ba00000000 10034 1 194915 185 13316 138 194915 185 0 0 0 0 13316 138 0 0 0 0
+156 wlan0 0xc9395b2600000000 10034 0 6991 13 6215 14 6991 13 0 0 0 0 6215 14 0 0 0 0
+157 wlan0 0xc9395b2600000000 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+158 wlan0 0xdaddf21100000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+159 wlan0 0xdaddf21100000000 10034 1 928676 849 81570 799 928676 849 0 0 0 0 81570 799 0 0 0 0
+160 wlan0 0xe8d195d100000000 10020 0 516 8 288 4 516 8 0 0 0 0 288 4 0 0 0 0
+161 wlan0 0xe8d195d100000000 10020 1 5905 15 2622 15 5905 15 0 0 0 0 2622 15 0 0 0 0
+162 wlan0 0xe8d195d100000000 10034 0 236640 524 312523 555 236640 524 0 0 0 0 312523 555 0 0 0 0
+163 wlan0 0xe8d195d100000000 10034 1 319028 539 188776 553 319028 539 0 0 0 0 188776 553 0 0 0 0
+164 wlan0 0xffffff0100000000 10006 0 80755 92 9122 99 80755 92 0 0 0 0 9122 99 0 0 0 0
+165 wlan0 0xffffff0100000000 10006 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+166 wlan0 0xffffff0100000000 10020 0 17874405 14068 223987 3065 17874405 14068 0 0 0 0 223987 3065 0 0 0 0
+167 wlan0 0xffffff0100000000 10020 1 11011258 8672 177693 2407 11011258 8672 0 0 0 0 177693 2407 0 0 0 0
+168 wlan0 0xffffff0100000000 10034 0 436062595 341880 5843990 79630 436062595 341880 0 0 0 0 5843990 79630 0 0 0 0
+169 wlan0 0xffffff0100000000 10034 1 63201220 49447 1005882 13713 63201220 49447 0 0 0 0 1005882 13713 0 0 0 0
+170 wlan0 0xffffff0100000000 10044 0 17159287 13702 356212 4778 17159287 13702 0 0 0 0 356212 4778 0 0 0 0
+171 wlan0 0xffffff0100000000 10044 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+172 wlan0 0xffffff0100000000 10078 0 10439 17 1665 15 10439 17 0 0 0 0 1665 15 0 0 0 0
+173 wlan0 0xffffff0100000000 10078 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+174 wlan0 0xffffff0100000000 10090 0 23722655 19697 881995 14231 23722655 19697 0 0 0 0 881995 14231 0 0 0 0
+175 wlan0 0xffffff0100000000 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+176 wlan0 0xffffff0500000000 1000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+177 wlan0 0xffffff0500000000 1000 1 1592 5 314 1 0 0 1592 5 0 0 0 0 314 1 0 0
+178 wlan0 0xffffff0600000000 1000 0 0 0 36960 385 0 0 0 0 0 0 0 0 36960 385 0 0
+179 wlan0 0xffffff0600000000 1000 1 96 1 480 5 0 0 96 1 0 0 0 0 480 5 0 0
+180 wlan0 0xffffff0700000000 1000 0 38732 229 16567 163 38732 229 0 0 0 0 16567 163 0 0 0 0
+181 wlan0 0xffffff0700000000 1000 1 18539 74 7562 66 18539 74 0 0 0 0 7562 66 0 0 0 0
+182 wlan0 0xffffff0900000000 1000 0 38381 43 2624 27 38381 43 0 0 0 0 2624 27 0 0 0 0
+183 wlan0 0xffffff0900000000 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+184 dummy0 0x0 0 0 0 0 168 3 0 0 0 0 0 0 0 0 0 0 168 3
+185 dummy0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+186 wlan0 0x0 1029 0 0 0 5855801 94173 0 0 0 0 0 0 5208040 84634 103637 1256 544124 8283
+187 wlan0 0x0 1029 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
diff --git a/tests/unit/res/raw/xt_qtaguid_with_clat_simple b/tests/unit/res/raw/xt_qtaguid_with_clat_simple
new file mode 100644
index 0000000..a1d6d41
--- /dev/null
+++ b/tests/unit/res/raw/xt_qtaguid_with_clat_simple
@@ -0,0 +1,4 @@
+idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
+2 v4-wlan0 0x0 10060 0 42600 213 4100 41 42600 213 0 0 0 0 4100 41 0 0 0 0
+3 v4-wlan0 0x0 10060 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+4 wlan0 0x0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0