Merge "Apply jarjar rules on coverage tests"
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index bd7ebda..88c885a 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -67,6 +67,20 @@
     name: "com.android.tethering-bootclasspath-fragment",
     contents: ["framework-tethering"],
     apex_available: ["com.android.tethering"],
+
+    // The bootclasspath_fragments that provide APIs on which this depends.
+    fragments: [
+        {
+            apex: "com.android.art",
+            module: "art-bootclasspath-fragment",
+        },
+    ],
+
+    // Additional stubs libraries that this fragment's contents use which are
+    // not provided by another bootclasspath_fragment.
+    additional_stubs: [
+        "android-non-updatable",
+    ],
 }
 
 override_apex {
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 33f1c29..a33af61 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
@@ -33,6 +33,8 @@
 import com.android.networkstack.tethering.Tether4Value;
 import com.android.networkstack.tethering.TetherStatsValue;
 
+import java.util.function.BiConsumer;
+
 /**
  * Bpf coordinator class for API shims.
  */
@@ -161,6 +163,12 @@
     }
 
     @Override
+    public void tetherOffloadRuleForEach(boolean downstream,
+            @NonNull BiConsumer<Tether4Key, Tether4Value> action) {
+        /* no op */
+    }
+
+    @Override
     public boolean attachProgram(String iface, boolean downstream) {
         /* no op */
         return true;
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 74ddcbc..611c828 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
@@ -47,6 +47,7 @@
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.util.function.BiConsumer;
 
 /**
  * Bpf coordinator class for API shims.
@@ -380,10 +381,7 @@
 
         try {
             if (downstream) {
-                if (!mBpfDownstream4Map.deleteEntry(key)) {
-                    mLog.e("Could not delete entry (key: " + key + ")");
-                    return false;
-                }
+                if (!mBpfDownstream4Map.deleteEntry(key)) return false;  // Rule did not exist
 
                 // Decrease the rule count while a deleting rule is not using a given upstream
                 // interface anymore.
@@ -401,19 +399,32 @@
                     mRule4CountOnUpstream.put(upstreamIfindex, count);
                 }
             } else {
-                mBpfUpstream4Map.deleteEntry(key);
+                if (!mBpfUpstream4Map.deleteEntry(key)) return false;  // Rule did not exist
             }
         } catch (ErrnoException e) {
-            // Silent if the rule did not exist.
-            if (e.errno != OsConstants.ENOENT) {
-                mLog.e("Could not delete entry: ", e);
-                return false;
-            }
+            mLog.e("Could not delete entry (key: " + key + ")", e);
+            return false;
         }
         return true;
     }
 
     @Override
+    public void tetherOffloadRuleForEach(boolean downstream,
+            @NonNull BiConsumer<Tether4Key, Tether4Value> action) {
+        if (!isInitialized()) return;
+
+        try {
+            if (downstream) {
+                mBpfDownstream4Map.forEach(action);
+            } else {
+                mBpfUpstream4Map.forEach(action);
+            }
+        } catch (ErrnoException e) {
+            mLog.e("Could not iterate map: ", e);
+        }
+    }
+
+    @Override
     public boolean attachProgram(String iface, boolean downstream) {
         if (!isInitialized()) return false;
 
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 8a7a49c..08ab9ca 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
@@ -28,6 +28,8 @@
 import com.android.networkstack.tethering.Tether4Value;
 import com.android.networkstack.tethering.TetherStatsValue;
 
+import java.util.function.BiConsumer;
+
 /**
  * Bpf coordinator class for API shims.
  */
@@ -145,10 +147,25 @@
 
     /**
      * Deletes a tethering IPv4 offload rule from the appropriate BPF map.
+     *
+     * @param downstream true if downstream, false if upstream.
+     * @param key the key to delete.
+     * @return true iff the map was modified, false if the key did not exist or there was an error.
      */
     public abstract boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key);
 
     /**
+     * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
+     *
+     * @param downstream true if downstream, false if upstream.
+     * @param action represents the action for each key -> value. The entry deletion is not
+     *        allowed and use #tetherOffloadRuleRemove instead.
+     */
+    @Nullable
+    public abstract void tetherOffloadRuleForEach(boolean downstream,
+            @NonNull BiConsumer<Tether4Key, Tether4Value> action);
+
+    /**
      * Whether there is currently any IPv4 rule on the specified upstream.
      */
     public abstract boolean isAnyIpv4RuleOnUpstream(int ifIndex);
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 3428c1d..822bdf6 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -596,6 +596,7 @@
         // into calls to InterfaceController, shared with startIPv4().
         mInterfaceCtrl.clearIPv4Address();
         mPrivateAddressCoordinator.releaseDownstream(this);
+        mBpfCoordinator.tetherOffloadClientClear(this);
         mIpv4Address = null;
         mStaticIpv4ServerAddr = null;
         mStaticIpv4ClientAddr = null;
@@ -949,7 +950,6 @@
         if (e.isValid()) {
             mBpfCoordinator.tetherOffloadClientAdd(this, clientInfo);
         } else {
-            // TODO: Delete all related offload rules which are using this client.
             mBpfCoordinator.tetherOffloadClientRemove(this, clientInfo);
         }
     }
@@ -1283,6 +1283,16 @@
             super.exit();
         }
 
+        // Note that IPv4 offload rules cleanup is implemented in BpfCoordinator while upstream
+        // state is null or changed because IPv4 and IPv6 tethering have different code flow
+        // and behaviour. While upstream is switching from offload supported interface to
+        // offload non-supportted interface, event CMD_TETHER_CONNECTION_CHANGED calls
+        // #cleanupUpstreamInterface but #cleanupUpstream because new UpstreamIfaceSet is not null.
+        // This case won't happen in IPv6 tethering because IPv6 tethering upstream state is
+        // reported by IPv6TetheringCoordinator. #cleanupUpstream is also called by unwirding
+        // adding NAT failure. In that case, the IPv4 offload rules are removed by #stopIPv4
+        // in the state machine. Once there is any case out whish is not covered by previous cases,
+        // probably consider clearing rules in #cleanupUpstream as well.
         private void cleanupUpstream() {
             if (mUpstreamIfaceSet == null) return;
 
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 4a05c9f..56dc69c 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -34,7 +34,6 @@
 
 import android.app.usage.NetworkStatsManager;
 import android.net.INetd;
-import android.net.LinkProperties;
 import android.net.MacAddress;
 import android.net.NetworkStats;
 import android.net.NetworkStats.Entry;
@@ -42,7 +41,9 @@
 import android.net.ip.ConntrackMonitor;
 import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
 import android.net.ip.IpServer;
+import android.net.netlink.ConntrackMessage;
 import android.net.netlink.NetlinkConstants;
+import android.net.netlink.NetlinkSocket;
 import android.net.netstats.provider.NetworkStatsProvider;
 import android.net.util.InterfaceParams;
 import android.net.util.SharedLog;
@@ -50,7 +51,9 @@
 import android.os.Handler;
 import android.os.SystemClock;
 import android.system.ErrnoException;
+import android.system.OsConstants;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -69,6 +72,7 @@
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -120,6 +124,13 @@
     }
 
     @VisibleForTesting
+    static final int POLLING_CONNTRACK_TIMEOUT_MS = 60_000;
+    @VisibleForTesting
+    static final int NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED = 432000;
+    @VisibleForTesting
+    static final int NF_CONNTRACK_UDP_TIMEOUT_STREAM = 180;
+
+    @VisibleForTesting
     enum StatsType {
         STATS_PER_IFACE,
         STATS_PER_UID,
@@ -228,12 +239,22 @@
     // BpfCoordinatorTest needs predictable iteration order.
     private final Set<Integer> mDeviceMapSet = new LinkedHashSet<>();
 
+    // Tracks the last IPv4 upstream index. Support single upstream only.
+    // TODO: Support multi-upstream interfaces.
+    private int mLastIPv4UpstreamIfindex = 0;
+
     // Runnable that used by scheduling next polling of stats.
-    private final Runnable mScheduledPollingTask = () -> {
+    private final Runnable mScheduledPollingStats = () -> {
         updateForwardedStats();
         maybeSchedulePollingStats();
     };
 
+    // Runnable that used by scheduling next polling of conntrack timeout.
+    private final Runnable mScheduledPollingConntrackTimeout = () -> {
+        maybeRefreshConntrackTimeout();
+        maybeSchedulePollingConntrackTimeout();
+    };
+
     // TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
     @VisibleForTesting
     public abstract static class Dependencies {
@@ -263,13 +284,19 @@
         }
 
         /**
+         * Represents an estimate of elapsed time since boot in nanoseconds.
+         */
+        public long elapsedRealtimeNanos() {
+            return SystemClock.elapsedRealtimeNanos();
+        }
+
+        /**
          * Check OS Build at least S.
          *
          * TODO: move to BpfCoordinatorShim once the test doesn't need the mocked OS build for
          * testing different code flows concurrently.
          */
         public boolean isAtLeastS() {
-            // TODO: consider using ShimUtils.isAtLeastS.
             return SdkLevel.isAtLeastS();
         }
 
@@ -407,6 +434,7 @@
 
         mPollingStarted = true;
         maybeSchedulePollingStats();
+        maybeSchedulePollingConntrackTimeout();
 
         mLog.i("Polling started");
     }
@@ -422,9 +450,13 @@
     public void stopPolling() {
         if (!mPollingStarted) return;
 
-        // Stop scheduled polling tasks and poll the latest stats from BPF maps.
-        if (mHandler.hasCallbacks(mScheduledPollingTask)) {
-            mHandler.removeCallbacks(mScheduledPollingTask);
+        // Stop scheduled polling conntrack timeout.
+        if (mHandler.hasCallbacks(mScheduledPollingConntrackTimeout)) {
+            mHandler.removeCallbacks(mScheduledPollingConntrackTimeout);
+        }
+        // Stop scheduled polling stats and poll the latest stats from BPF maps.
+        if (mHandler.hasCallbacks(mScheduledPollingStats)) {
+            mHandler.removeCallbacks(mScheduledPollingStats);
         }
         updateForwardedStats();
         mPollingStarted = false;
@@ -576,6 +608,7 @@
     /**
      * Clear all forwarding rules for a given downstream.
      * Note that this can be only called on handler thread.
+     * TODO: rename to tetherOffloadRuleClear6 because of IPv6 only.
      */
     public void tetherOffloadRuleClear(@NonNull final IpServer ipServer) {
         if (!isUsingBpf()) return;
@@ -647,6 +680,7 @@
 
     /**
      * Add downstream client.
+     * Note that this can be only called on handler thread.
      */
     public void tetherOffloadClientAdd(@NonNull final IpServer ipServer,
             @NonNull final ClientInfo client) {
@@ -661,54 +695,180 @@
     }
 
     /**
-     * Remove downstream client.
+     * Remove a downstream client and its rules if any.
+     * Note that this can be only called on handler thread.
      */
     public void tetherOffloadClientRemove(@NonNull final IpServer ipServer,
             @NonNull final ClientInfo client) {
         if (!isUsingBpf()) return;
 
+        // No clients on the downstream, return early.
         HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
         if (clients == null) return;
 
-        // If no rule is removed, return early. Avoid unnecessary work on a non-existent rule
-        // which may have never been added or removed already.
+        // No client is removed, return early.
         if (clients.remove(client.clientAddress) == null) return;
 
-        // Remove the downstream entry if it has no more rule.
+        // Remove the client's rules. Removing the client implies that its rules are not used
+        // anymore.
+        tetherOffloadRuleClear(client);
+
+        // Remove the downstream entry if it has no more client.
         if (clients.isEmpty()) {
             mTetherClients.remove(ipServer);
         }
     }
 
     /**
-     * Call when UpstreamNetworkState may be changed.
-     * If upstream has ipv4 for tethering, update this new UpstreamNetworkState to map. The
-     * upstream interface index and its address mapping is prepared for building IPv4
-     * offload rule.
-     *
-     * TODO: Delete the unused upstream interface mapping.
-     * TODO: Support ether ip upstream interface.
+     * Clear all downstream clients and their rules if any.
+     * Note that this can be only called on handler thread.
      */
-    public void addUpstreamIfindexToMap(LinkProperties lp) {
-        if (!mPollingStarted) return;
+    public void tetherOffloadClientClear(@NonNull final IpServer ipServer) {
+        if (!isUsingBpf()) return;
+
+        final HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
+        if (clients == null) return;
+
+        // Need to build a client list because the client map may be changed in the iteration.
+        for (final ClientInfo c : new ArrayList<ClientInfo>(clients.values())) {
+            tetherOffloadClientRemove(ipServer, c);
+        }
+    }
+
+    /**
+     * Clear all forwarding IPv4 rules for a given client.
+     * Note that this can be only called on handler thread.
+     */
+    private void tetherOffloadRuleClear(@NonNull final ClientInfo clientInfo) {
+        // TODO: consider removing the rules in #tetherOffloadRuleForEach once BpfMap#forEach
+        // can guarantee that deleting some pass-in rules in the BPF map iteration can still
+        // walk through every entry.
+        final Inet4Address clientAddr = clientInfo.clientAddress;
+        final Set<Integer> upstreamIndiceSet = new ArraySet<Integer>();
+        final Set<Tether4Key> deleteUpstreamRuleKeys = new ArraySet<Tether4Key>();
+        final Set<Tether4Key> deleteDownstreamRuleKeys = new ArraySet<Tether4Key>();
+
+        // Find the rules which are related with the given client.
+        mBpfCoordinatorShim.tetherOffloadRuleForEach(UPSTREAM, (k, v) -> {
+            if (Arrays.equals(k.src4, clientAddr.getAddress())) {
+                deleteUpstreamRuleKeys.add(k);
+            }
+        });
+        mBpfCoordinatorShim.tetherOffloadRuleForEach(DOWNSTREAM, (k, v) -> {
+            if (Arrays.equals(v.dst46, toIpv4MappedAddressBytes(clientAddr))) {
+                deleteDownstreamRuleKeys.add(k);
+                upstreamIndiceSet.add((int) k.iif);
+            }
+        });
+
+        // The rules should be paired on upstream and downstream map because they are added by
+        // conntrack events which have bidirectional information.
+        // TODO: Consider figuring out a way to fix. Probably delete all rules to fallback.
+        if (deleteUpstreamRuleKeys.size() != deleteDownstreamRuleKeys.size()) {
+            Log.wtf(TAG, "The deleting rule numbers are different on upstream4 and downstream4 ("
+                    + "upstream: " + deleteUpstreamRuleKeys.size() + ", "
+                    + "downstream: " + deleteDownstreamRuleKeys.size() + ").");
+            return;
+        }
+
+        // Delete the rules which are related with the given client.
+        for (final Tether4Key k : deleteUpstreamRuleKeys) {
+            mBpfCoordinatorShim.tetherOffloadRuleRemove(UPSTREAM, k);
+        }
+        for (final Tether4Key k : deleteDownstreamRuleKeys) {
+            mBpfCoordinatorShim.tetherOffloadRuleRemove(DOWNSTREAM, k);
+        }
+
+        // Cleanup each upstream interface by a set which avoids duplicated work on the same
+        // upstream interface. Cleaning up the same interface twice (or more) here may raise
+        // an exception because all related information were removed in the first deletion.
+        for (final int upstreamIndex : upstreamIndiceSet) {
+            maybeClearLimit(upstreamIndex);
+        }
+    }
+
+    /**
+     * Clear all forwarding IPv4 rules for a given downstream. Needed because the client may still
+     * connect on the downstream but the existing rules are not required anymore. Ex: upstream
+     * changed.
+     */
+    private void tetherOffloadRule4Clear(@NonNull final IpServer ipServer) {
+        if (!isUsingBpf()) return;
+
+        final HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
+        if (clients == null) return;
+
+        // The value should be unique as its key because currently the key was using from its
+        // client address of ClientInfo. See #tetherOffloadClientAdd.
+        for (final ClientInfo client : clients.values()) {
+            tetherOffloadRuleClear(client);
+        }
+    }
+
+    private boolean isValidUpstreamIpv4Address(@NonNull final InetAddress addr) {
+        if (!(addr instanceof Inet4Address)) return false;
+        Inet4Address v4 = (Inet4Address) addr;
+        if (v4.isAnyLocalAddress() || v4.isLinkLocalAddress()
+                || v4.isLoopbackAddress() || v4.isMulticastAddress()) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Call when UpstreamNetworkState may be changed.
+     * If upstream has ipv4 for tethering, update this new UpstreamNetworkState
+     * to BpfCoordinator for building upstream interface index mapping. Otherwise,
+     * clear the all existing rules if any.
+     *
+     * Note that this can be only called on handler thread.
+     */
+    public void updateUpstreamNetworkState(UpstreamNetworkState ns) {
+        if (!isUsingBpf()) return;
+
+        int upstreamIndex = 0;
 
         // This will not work on a network that is using 464xlat because hasIpv4Address will not be
         // true.
         // TODO: need to consider 464xlat.
-        if (lp == null || !lp.hasIpv4Address()) return;
+        if (ns != null && ns.linkProperties != null && ns.linkProperties.hasIpv4Address()) {
+            // TODO: support ether ip upstream interface.
+            final InterfaceParams params = mDeps.getInterfaceParams(
+                    ns.linkProperties.getInterfaceName());
+            if (params != null && !params.hasMacAddress /* raw ip upstream only */) {
+                upstreamIndex = params.index;
+            }
+        }
+        if (mLastIPv4UpstreamIfindex == upstreamIndex) return;
 
-        // Support raw ip upstream interface only.
-        final InterfaceParams params = mDeps.getInterfaceParams(lp.getInterfaceName());
-        if (params == null || params.hasMacAddress) return;
+        // Clear existing rules if upstream interface is changed. The existing rules should be
+        // cleared before upstream index mapping is cleared. It can avoid that ipServer or
+        // conntrack event may use the non-existing upstream interfeace index to build a removing
+        // key while removeing the rules. Can't notify each IpServer to clear the rules as
+        // IPv6TetheringCoordinator#updateUpstreamNetworkState because the IpServer may not
+        // handle the upstream changing notification before changing upstream index mapping.
+        if (mLastIPv4UpstreamIfindex != 0) {
+            // Clear all forwarding IPv4 rules for all downstreams.
+            for (final IpServer ipserver : mTetherClients.keySet()) {
+                tetherOffloadRule4Clear(ipserver);
+            }
+        }
 
-        Collection<InetAddress> addresses = lp.getAddresses();
-        for (InetAddress addr: addresses) {
-            if (addr instanceof Inet4Address) {
-                Inet4Address i4addr = (Inet4Address) addr;
-                if (!i4addr.isAnyLocalAddress() && !i4addr.isLinkLocalAddress()
-                        && !i4addr.isLoopbackAddress() && !i4addr.isMulticastAddress()) {
-                    mIpv4UpstreamIndices.put(i4addr, params.index);
-                }
+        // Don't update mLastIPv4UpstreamIfindex before clearing existing rules if any. Need that
+        // to tell if it is required to clean the out-of-date rules.
+        mLastIPv4UpstreamIfindex = upstreamIndex;
+
+        // If link properties are valid, build the upstream information mapping. Otherwise, clear
+        // the upstream interface index mapping, to ensure that any conntrack events that arrive
+        // after the upstream is lost do not incorrectly add rules pointing at the upstream.
+        if (upstreamIndex == 0) {
+            mIpv4UpstreamIndices.clear();
+            return;
+        }
+        Collection<InetAddress> addresses = ns.linkProperties.getAddresses();
+        for (final InetAddress addr: addresses) {
+            if (isValidUpstreamIpv4Address(addr)) {
+                mIpv4UpstreamIndices.put((Inet4Address) addr, upstreamIndex);
             }
         }
     }
@@ -793,6 +953,24 @@
         dumpDevmap(pw);
         pw.decreaseIndent();
 
+        pw.println("Client Information:");
+        pw.increaseIndent();
+        if (mTetherClients.isEmpty()) {
+            pw.println("<empty>");
+        } else {
+            pw.println(mTetherClients.toString());
+        }
+        pw.decreaseIndent();
+
+        pw.println("IPv4 Upstream Indices:");
+        pw.increaseIndent();
+        if (mIpv4UpstreamIndices.isEmpty()) {
+            pw.println("<empty>");
+        } else {
+            pw.println(mIpv4UpstreamIndices.toString());
+        }
+        pw.decreaseIndent();
+
         pw.println();
         pw.println("Forwarding counters:");
         pw.increaseIndent();
@@ -891,11 +1069,12 @@
             throw new AssertionError("IP address array not valid IPv4 address!");
         }
 
-        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",
+        final String ageStr = (value.lastUsed == 0) ? "-"
+                : String.format("%dms", (now - value.lastUsed) / 1_000_000);
+        return String.format("[%s] %d(%s) %s:%d -> %d(%s) %s:%d -> %s:%d [%s] %s",
                 key.dstMac, key.iif, getIfName(key.iif), src4, key.srcPort,
                 value.oif, getIfName(value.oif),
-                public4, publicPort, dst4, value.dstPort, value.ethDstMac, ageMs);
+                public4, publicPort, dst4, value.dstPort, value.ethDstMac, ageStr);
     }
 
     private void dumpIpv4ForwardingRuleMap(long now, boolean downstream,
@@ -971,14 +1150,14 @@
                 return;
             }
             if (map.isEmpty()) {
-                pw.println("No interface index");
+                pw.println("<empty>");
                 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
+                // TODO: get downstream interface name because the index is either upstream or
                 // downstream interface in dev map.
                 pw.println(String.format("%d (%s) -> %d (%s)", k.ifIndex, getIfName(k.ifIndex),
                         v.ifIndex, getIfName(v.ifIndex)));
@@ -1248,12 +1427,99 @@
         return null;
     }
 
-    // Support raw ip only.
-    // TODO: add ether ip support.
+    @NonNull
+    private byte[] toIpv4MappedAddressBytes(Inet4Address ia4) {
+        final byte[] addr4 = ia4.getAddress();
+        final byte[] addr6 = new byte[16];
+        addr6[10] = (byte) 0xff;
+        addr6[11] = (byte) 0xff;
+        addr6[12] = addr4[0];
+        addr6[13] = addr4[1];
+        addr6[14] = addr4[2];
+        addr6[15] = addr4[3];
+        return addr6;
+    }
+
+    @Nullable
+    private Inet4Address ipv4MappedAddressBytesToIpv4Address(final byte[] addr46) {
+        if (addr46.length != 16) return null;
+        if (addr46[0] != 0 || addr46[1] != 0 || addr46[2] != 0 || addr46[3] != 0
+                || addr46[4] != 0 || addr46[5] != 0 || addr46[6] != 0 || addr46[7] != 0
+                || addr46[8] != 0 && addr46[9] != 0 || (addr46[10] & 0xff) != 0xff
+                || (addr46[11] & 0xff) != 0xff) {
+            return null;
+        }
+
+        final byte[] addr4 = new byte[4];
+        addr4[0] = addr46[12];
+        addr4[1] = addr46[13];
+        addr4[2] = addr46[14];
+        addr4[3] = addr46[15];
+
+        return parseIPv4Address(addr4);
+    }
+
     // TODO: parse CTA_PROTOINFO of conntrack event in ConntrackMonitor. For TCP, only add rules
     // while TCP status is established.
     @VisibleForTesting
     class BpfConntrackEventConsumer implements ConntrackEventConsumer {
+        // The upstream4 and downstream4 rules are built as the following tables. Only raw ip
+        // upstream interface is supported. Note that the field "lastUsed" is only updated by
+        // BPF program which records the last used time for a given rule.
+        // TODO: support ether ip upstream interface.
+        //
+        // NAT network topology:
+        //
+        //         public network (rawip)                 private network
+        //                   |                 UE                |
+        // +------------+    V    +------------+------------+    V    +------------+
+        // |   Sever    +---------+  Upstream  | Downstream +---------+   Client   |
+        // +------------+         +------------+------------+         +------------+
+        //
+        // upstream4 key and value:
+        //
+        // +------+------------------------------------------------+
+        // |      |      TetherUpstream4Key                        |
+        // +------+------+------+------+------+------+------+------+
+        // |field |iif   |dstMac|l4prot|src4  |dst4  |srcPor|dstPor|
+        // |      |      |      |o     |      |      |t     |t     |
+        // +------+------+------+------+------+------+------+------+
+        // |value |downst|downst|tcp/  |client|server|client|server|
+        // |      |ream  |ream  |udp   |      |      |      |      |
+        // +------+------+------+------+------+------+------+------+
+        //
+        // +------+---------------------------------------------------------------------+
+        // |      |      TetherUpstream4Value                                           |
+        // +------+------+------+------+------+------+------+------+------+------+------+
+        // |field |oif   |ethDst|ethSrc|ethPro|pmtu  |src46 |dst46 |srcPor|dstPor|lastUs|
+        // |      |      |mac   |mac   |to    |      |      |      |t     |t     |ed    |
+        // +------+------+------+------+------+------+------+------+------+------+------+
+        // |value |upstre|--    |--    |ETH_P_|1500  |upstre|server|upstre|server|--    |
+        // |      |am    |      |      |IP    |      |am    |      |am    |      |      |
+        // +------+------+------+------+------+------+------+------+------+------+------+
+        //
+        // downstream4 key and value:
+        //
+        // +------+------------------------------------------------+
+        // |      |      TetherDownstream4Key                      |
+        // +------+------+------+------+------+------+------+------+
+        // |field |iif   |dstMac|l4prot|src4  |dst4  |srcPor|dstPor|
+        // |      |      |      |o     |      |      |t     |t     |
+        // +------+------+------+------+------+------+------+------+
+        // |value |upstre|--    |tcp/  |server|upstre|server|upstre|
+        // |      |am    |      |udp   |      |am    |      |am    |
+        // +------+------+------+------+------+------+------+------+
+        //
+        // +------+---------------------------------------------------------------------+
+        // |      |      TetherDownstream4Value                                         |
+        // +------+------+------+------+------+------+------+------+------+------+------+
+        // |field |oif   |ethDst|ethSrc|ethPro|pmtu  |src46 |dst46 |srcPor|dstPor|lastUs|
+        // |      |      |mac   |mac   |to    |      |      |      |t     |t     |ed    |
+        // +------+------+------+------+------+------+------+------+------+------+------+
+        // |value |downst|client|downst|ETH_P_|1500  |server|client|server|client|--    |
+        // |      |ream  |      |ream  |IP    |      |      |      |      |      |      |
+        // +------+------+------+------+------+------+------+------+------+------+------+
+        //
         @NonNull
         private Tether4Key makeTetherUpstream4Key(
                 @NonNull ConntrackEvent e, @NonNull ClientInfo c) {
@@ -1292,19 +1558,6 @@
                     0 /* lastUsed, filled by bpf prog only */);
         }
 
-        @NonNull
-        private byte[] toIpv4MappedAddressBytes(Inet4Address ia4) {
-            final byte[] addr4 = ia4.getAddress();
-            final byte[] addr6 = new byte[16];
-            addr6[10] = (byte) 0xff;
-            addr6[11] = (byte) 0xff;
-            addr6[12] = addr4[0];
-            addr6[13] = addr4[1];
-            addr6[14] = addr4[2];
-            addr6[15] = addr4[3];
-            return addr6;
-        }
-
         public void accept(ConntrackEvent e) {
             final ClientInfo tetherClient = getClientInfo(e.tupleOrig.srcIp);
             if (tetherClient == null) return;
@@ -1318,8 +1571,23 @@
 
             if (e.msgType == (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8
                     | NetlinkConstants.IPCTNL_MSG_CT_DELETE)) {
-                mBpfCoordinatorShim.tetherOffloadRuleRemove(UPSTREAM, upstream4Key);
-                mBpfCoordinatorShim.tetherOffloadRuleRemove(DOWNSTREAM, downstream4Key);
+                final boolean deletedUpstream = mBpfCoordinatorShim.tetherOffloadRuleRemove(
+                        UPSTREAM, upstream4Key);
+                final boolean deletedDownstream = mBpfCoordinatorShim.tetherOffloadRuleRemove(
+                        DOWNSTREAM, downstream4Key);
+
+                if (!deletedUpstream && !deletedDownstream) {
+                    // The rules may have been already removed by losing client or losing upstream.
+                    return;
+                }
+
+                if (deletedUpstream != deletedDownstream) {
+                    Log.wtf(TAG, "The bidirectional rules should be removed concurrently ("
+                            + "upstream: " + deletedUpstream
+                            + ", downstream: " + deletedDownstream + ")");
+                    return;
+                }
+
                 maybeClearLimit(upstreamIndex);
                 return;
             }
@@ -1585,14 +1853,89 @@
         return Math.max(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, configInterval);
     }
 
+    @Nullable
+    private Inet4Address parseIPv4Address(byte[] addrBytes) {
+        try {
+            final InetAddress ia = Inet4Address.getByAddress(addrBytes);
+            if (ia instanceof Inet4Address) return (Inet4Address) ia;
+        } catch (UnknownHostException | IllegalArgumentException e) {
+            mLog.e("Failed to parse IPv4 address: " + e);
+        }
+        return null;
+    }
+
+    // Update CTA_TUPLE_ORIG timeout for a given conntrack entry. Note that there will also be
+    // coming a conntrack event to notify updated timeout.
+    private void updateConntrackTimeout(byte proto, Inet4Address src4, short srcPort,
+            Inet4Address dst4, short dstPort) {
+        if (src4 == null || dst4 == null) return;
+
+        // TODO: consider acquiring the timeout setting from nf_conntrack_* variables.
+        // - proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established
+        // - proc/sys/net/netfilter/nf_conntrack_udp_timeout_stream
+        // See kernel document nf_conntrack-sysctl.txt.
+        final int timeoutSec = (proto == OsConstants.IPPROTO_TCP)
+                ? NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED
+                : NF_CONNTRACK_UDP_TIMEOUT_STREAM;
+        final byte[] msg = ConntrackMessage.newIPv4TimeoutUpdateRequest(
+                proto, src4, (int) srcPort, dst4, (int) dstPort, timeoutSec);
+        try {
+            NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_NETFILTER, msg);
+        } catch (ErrnoException e) {
+            mLog.e("Error updating conntrack entry ("
+                    + "proto: " + proto + ", "
+                    + "src4: " + src4 + ", "
+                    + "srcPort: " + Short.toUnsignedInt(srcPort) + ", "
+                    + "dst4: " + dst4 + ", "
+                    + "dstPort: " + Short.toUnsignedInt(dstPort) + "), "
+                    + "msg: " + NetlinkConstants.hexify(msg) + ", "
+                    + "e: " + e);
+        }
+    }
+
+    private void maybeRefreshConntrackTimeout() {
+        final long now = mDeps.elapsedRealtimeNanos();
+
+        // Reverse the source and destination {address, port} from downstream value because
+        // #updateConntrackTimeout refresh the timeout of netlink attribute CTA_TUPLE_ORIG
+        // which is opposite direction for downstream map value.
+        mBpfCoordinatorShim.tetherOffloadRuleForEach(DOWNSTREAM, (k, v) -> {
+            if ((now - v.lastUsed) / 1_000_000 < POLLING_CONNTRACK_TIMEOUT_MS) {
+                updateConntrackTimeout((byte) k.l4proto,
+                        ipv4MappedAddressBytesToIpv4Address(v.dst46), (short) v.dstPort,
+                        ipv4MappedAddressBytesToIpv4Address(v.src46), (short) v.srcPort);
+            }
+        });
+
+        // TODO: Consider ignoring TCP traffic on upstream and monitor on downstream only
+        // because TCP is a bidirectional traffic. Probably don't need to extend timeout by
+        // both directions for TCP.
+        mBpfCoordinatorShim.tetherOffloadRuleForEach(UPSTREAM, (k, v) -> {
+            if ((now - v.lastUsed) / 1_000_000 < POLLING_CONNTRACK_TIMEOUT_MS) {
+                updateConntrackTimeout((byte) k.l4proto, parseIPv4Address(k.src4),
+                        (short) k.srcPort, parseIPv4Address(k.dst4), (short) k.dstPort);
+            }
+        });
+    }
+
     private void maybeSchedulePollingStats() {
         if (!mPollingStarted) return;
 
-        if (mHandler.hasCallbacks(mScheduledPollingTask)) {
-            mHandler.removeCallbacks(mScheduledPollingTask);
+        if (mHandler.hasCallbacks(mScheduledPollingStats)) {
+            mHandler.removeCallbacks(mScheduledPollingStats);
         }
 
-        mHandler.postDelayed(mScheduledPollingTask, getPollingInterval());
+        mHandler.postDelayed(mScheduledPollingStats, getPollingInterval());
+    }
+
+    private void maybeSchedulePollingConntrackTimeout() {
+        if (!mPollingStarted) return;
+
+        if (mHandler.hasCallbacks(mScheduledPollingConntrackTimeout)) {
+            mHandler.removeCallbacks(mScheduledPollingConntrackTimeout);
+        }
+
+        mHandler.postDelayed(mScheduledPollingConntrackTimeout, POLLING_CONNTRACK_TIMEOUT_MS);
     }
 
     // Return forwarding rule map. This is used for testing only.
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index b52ec86..079bf9c 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -1039,7 +1039,7 @@
             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 ncm:%s",
+            mLog.i(String.format("USB bcast connected:%s configured:%s rndis:%s ncm:%s",
                     usbConnected, usbConfigured, rndisEnabled, ncmEnabled));
 
             // There are three types of ACTION_USB_STATE:
@@ -1416,7 +1416,7 @@
 
         // If TETHERING_USB is forced to use ncm function, TETHERING_NCM would no longer be
         // available.
-        if (mConfig.isUsingNcm()) return TETHER_ERROR_SERVICE_UNAVAIL;
+        if (mConfig.isUsingNcm() && enable) return TETHER_ERROR_SERVICE_UNAVAIL;
 
         UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
         usbManager.setCurrentFunctions(enable ? UsbManager.FUNCTION_NCM : UsbManager.FUNCTION_NONE);
@@ -1720,13 +1720,7 @@
         protected void handleNewUpstreamNetworkState(UpstreamNetworkState ns) {
             mIPv6TetheringCoordinator.updateUpstreamNetworkState(ns);
             mOffload.updateUpstreamNetworkState(ns);
-
-            // TODO: Delete all related offload rules which are using this upstream.
-            if (ns != null) {
-                // Add upstream index to the map. The upstream interface index is required while
-                // the conntrack event builds the offload rules.
-                mBpfCoordinator.addUpstreamIfindexToMap(ns.linkProperties);
-            }
+            mBpfCoordinator.updateUpstreamNetworkState(ns);
         }
 
         private void handleInterfaceServingStateActive(int mode, IpServer who) {
@@ -2336,6 +2330,9 @@
         pw.println("Tethering:");
         pw.increaseIndent();
 
+        pw.println("Callbacks registered: "
+                + mTetheringEventCallbacks.getRegisteredCallbackCount());
+
         pw.println("Configuration:");
         pw.increaseIndent();
         final TetheringConfiguration cfg = mConfig;
@@ -2558,7 +2555,7 @@
             return;
         }
 
-        mLog.log("adding IpServer for: " + iface);
+        mLog.i("adding IpServer for: " + iface);
         final TetherState tetherState = new TetherState(
                 new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
                              makeControlCallback(), mConfig.enableLegacyDhcpServer,
@@ -2573,7 +2570,7 @@
         if (tetherState == null) return;
 
         tetherState.ipServer.stop();
-        mLog.log("removing IpServer for: " + iface);
+        mLog.i("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 31fcea4..d2f44d3 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -176,7 +176,9 @@
         // us an interface name. Careful consideration needs to be given to
         // implications for Settings and for provisioning checks.
         tetherableWifiRegexs = getResourceStringArray(res, R.array.config_tether_wifi_regexs);
-        tetherableWigigRegexs = getResourceStringArray(res, R.array.config_tether_wigig_regexs);
+        // TODO: Remove entire wigig code once tethering module no longer support R devices.
+        tetherableWigigRegexs = SdkLevel.isAtLeastS()
+                ? new String[0] : getResourceStringArray(res, R.array.config_tether_wigig_regexs);
         tetherableWifiP2pRegexs = getResourceStringArray(
                 res, R.array.config_tether_wifi_p2p_regexs);
         tetherableBluetoothRegexs = getResourceStringArray(
diff --git a/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java b/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java
index 07aab63..ef254ff 100644
--- a/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java
+++ b/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java
@@ -22,7 +22,6 @@
 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;
@@ -102,8 +101,7 @@
 
         TestNetworkTracker tnt = null;
         try {
-            tetherEventCallback.assumeTetheringSupported();
-            assumeTrue(isWifiTetheringSupported(mContext, tetherEventCallback));
+            tetherEventCallback.assumeWifiTetheringSupported(mContext);
             tetherEventCallback.expectNoTetheringActive();
 
             final TetheringInterface tetheredIface =
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index ce69cb3..378a21c 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -584,6 +584,7 @@
         inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
         inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
         inOrder.verify(mAddressCoordinator).releaseDownstream(any());
+        inOrder.verify(mBpfCoordinator).tetherOffloadClientClear(mIpServer);
         inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer);
         inOrder.verify(mCallback).updateInterfaceState(
                 mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
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 cc912f4..914e0d4 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -36,9 +36,13 @@
 import static android.system.OsConstants.ETH_P_IPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.NETLINK_NETFILTER;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
+import static com.android.networkstack.tethering.BpfCoordinator.NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED;
+import static com.android.networkstack.tethering.BpfCoordinator.NF_CONNTRACK_UDP_TIMEOUT_STREAM;
+import static com.android.networkstack.tethering.BpfCoordinator.POLLING_CONNTRACK_TIMEOUT_MS;
 import static com.android.networkstack.tethering.BpfCoordinator.StatsType;
 import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE;
 import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID;
@@ -70,13 +74,17 @@
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.MacAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
 import android.net.NetworkStats;
 import android.net.TetherOffloadRuleParcel;
 import android.net.TetherStatsParcel;
 import android.net.ip.ConntrackMonitor;
 import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
 import android.net.ip.IpServer;
+import android.net.netlink.ConntrackMessage;
 import android.net.netlink.NetlinkConstants;
+import android.net.netlink.NetlinkSocket;
 import android.net.util.InterfaceParams;
 import android.net.util.SharedLog;
 import android.os.Build;
@@ -127,6 +135,8 @@
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
 
+    private static final int TEST_NET_ID = 24;
+
     private static final int UPSTREAM_IFINDEX = 1001;
     private static final int DOWNSTREAM_IFINDEX = 1002;
 
@@ -217,6 +227,7 @@
     // it has to access the non-static function of BPF coordinator.
     private BpfConntrackEventConsumer mConsumer;
 
+    private long mElapsedRealtimeNanos = 0;
     private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
             ArgumentCaptor.forClass(ArrayList.class);
     private final TestLooper mTestLooper = new TestLooper();
@@ -256,6 +267,10 @@
                         return mConntrackMonitor;
                     }
 
+                    public long elapsedRealtimeNanos() {
+                        return mElapsedRealtimeNanos;
+                    }
+
                     @Nullable
                     public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
                         return mBpfDownstream4Map;
@@ -1340,6 +1355,11 @@
     }
 
     @NonNull
+    private Tether4Key makeDownstream4Key() {
+        return makeDownstream4Key(IPPROTO_TCP);
+    }
+
+    @NonNull
     private ConntrackEvent makeTestConntrackEvent(short msgType, int proto) {
         if (msgType != IPCTNL_MSG_CT_NEW && msgType != IPCTNL_MSG_CT_DELETE) {
             fail("Not support message type " + msgType);
@@ -1365,7 +1385,10 @@
         final LinkProperties lp = new LinkProperties();
         lp.setInterfaceName(UPSTREAM_IFACE);
         lp.addLinkAddress(new LinkAddress(PUBLIC_ADDR, 32 /* prefix length */));
-        coordinator.addUpstreamIfindexToMap(lp);
+        final NetworkCapabilities capabilities = new NetworkCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+        coordinator.updateUpstreamNetworkState(new UpstreamNetworkState(lp, capabilities,
+                new Network(TEST_NET_ID)));
     }
 
     private void setDownstreamAndClientInformationTo(final BpfCoordinator coordinator) {
@@ -1379,8 +1402,11 @@
         // was started.
         coordinator.startPolling();
 
-        // Needed because tetherOffloadRuleRemove of api31.BpfCoordinatorShimImpl only decreases
-        // the count while the entry is deleted. In the other words, deleteEntry returns true.
+        // Needed because two reasons: (1) BpfConntrackEventConsumer#accept only performs cleanup
+        // when both upstream and downstream rules are removed. (2) tetherOffloadRuleRemove of
+        // api31.BpfCoordinatorShimImpl only decreases the count while the entry is deleted.
+        // In the other words, deleteEntry returns true.
+        doReturn(true).when(mBpfUpstream4Map).deleteEntry(any());
         doReturn(true).when(mBpfDownstream4Map).deleteEntry(any());
 
         // Needed because BpfCoordinator#addUpstreamIfindexToMap queries interface parameter for
@@ -1494,4 +1520,104 @@
         mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP));
         verify(mBpfDevMap, never()).updateEntry(any(), any());
     }
+
+    private void setElapsedRealtimeNanos(long nanoSec) {
+        mElapsedRealtimeNanos = nanoSec;
+    }
+
+    private void checkRefreshConntrackTimeout(final TestBpfMap<Tether4Key, Tether4Value> bpfMap,
+            final Tether4Key tcpKey, final Tether4Value tcpValue, final Tether4Key udpKey,
+            final Tether4Value udpValue) throws Exception {
+        // Both system elapsed time since boot and the rule last used time are used to measure
+        // the rule expiration. In this test, all test rules are fixed the last used time to 0.
+        // Set the different testing elapsed time to make the rule to be valid or expired.
+        //
+        // Timeline:
+        // 0                                       60 (seconds)
+        // +---+---+---+---+--...--+---+---+---+---+---+- ..
+        // |      POLLING_CONNTRACK_TIMEOUT_MS     |
+        // +---+---+---+---+--...--+---+---+---+---+---+- ..
+        // |<-          valid diff           ->|
+        // |<-          expired diff                 ->|
+        // ^                                   ^       ^
+        // last used time      elapsed time (valid)    elapsed time (expired)
+        final long validTime = (POLLING_CONNTRACK_TIMEOUT_MS - 1) * 1_000_000L;
+        final long expiredTime = (POLLING_CONNTRACK_TIMEOUT_MS + 1) * 1_000_000L;
+
+        // Static mocking for NetlinkSocket.
+        MockitoSession mockSession = ExtendedMockito.mockitoSession()
+                .mockStatic(NetlinkSocket.class)
+                .startMocking();
+        try {
+            final BpfCoordinator coordinator = makeBpfCoordinator();
+            coordinator.startPolling();
+            bpfMap.insertEntry(tcpKey, tcpValue);
+            bpfMap.insertEntry(udpKey, udpValue);
+
+            // [1] Don't refresh contrack timeout.
+            setElapsedRealtimeNanos(expiredTime);
+            mTestLooper.moveTimeForward(POLLING_CONNTRACK_TIMEOUT_MS);
+            waitForIdle();
+            ExtendedMockito.verifyNoMoreInteractions(staticMockMarker(NetlinkSocket.class));
+            ExtendedMockito.clearInvocations(staticMockMarker(NetlinkSocket.class));
+
+            // [2] Refresh contrack timeout.
+            setElapsedRealtimeNanos(validTime);
+            mTestLooper.moveTimeForward(POLLING_CONNTRACK_TIMEOUT_MS);
+            waitForIdle();
+            final byte[] expectedNetlinkTcp = ConntrackMessage.newIPv4TimeoutUpdateRequest(
+                    IPPROTO_TCP, PRIVATE_ADDR, (int) PRIVATE_PORT, REMOTE_ADDR,
+                    (int) REMOTE_PORT, NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED);
+            final byte[] expectedNetlinkUdp = ConntrackMessage.newIPv4TimeoutUpdateRequest(
+                    IPPROTO_UDP, PRIVATE_ADDR, (int) PRIVATE_PORT, REMOTE_ADDR,
+                    (int) REMOTE_PORT, NF_CONNTRACK_UDP_TIMEOUT_STREAM);
+            ExtendedMockito.verify(() -> NetlinkSocket.sendOneShotKernelMessage(
+                    eq(NETLINK_NETFILTER), eq(expectedNetlinkTcp)));
+            ExtendedMockito.verify(() -> NetlinkSocket.sendOneShotKernelMessage(
+                    eq(NETLINK_NETFILTER), eq(expectedNetlinkUdp)));
+            ExtendedMockito.verifyNoMoreInteractions(staticMockMarker(NetlinkSocket.class));
+            ExtendedMockito.clearInvocations(staticMockMarker(NetlinkSocket.class));
+
+            // [3] Don't refresh contrack timeout if polling stopped.
+            coordinator.stopPolling();
+            mTestLooper.moveTimeForward(POLLING_CONNTRACK_TIMEOUT_MS);
+            waitForIdle();
+            ExtendedMockito.verifyNoMoreInteractions(staticMockMarker(NetlinkSocket.class));
+            ExtendedMockito.clearInvocations(staticMockMarker(NetlinkSocket.class));
+        } finally {
+            mockSession.finishMocking();
+        }
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testRefreshConntrackTimeout_Upstream4Map() throws Exception {
+        // TODO: Replace the dependencies BPF map with a non-mocked TestBpfMap object.
+        final TestBpfMap<Tether4Key, Tether4Value> bpfUpstream4Map =
+                new TestBpfMap<>(Tether4Key.class, Tether4Value.class);
+        doReturn(bpfUpstream4Map).when(mDeps).getBpfUpstream4Map();
+
+        final Tether4Key tcpKey = makeUpstream4Key(IPPROTO_TCP);
+        final Tether4Key udpKey = makeUpstream4Key(IPPROTO_UDP);
+        final Tether4Value tcpValue = makeUpstream4Value();
+        final Tether4Value udpValue = makeUpstream4Value();
+
+        checkRefreshConntrackTimeout(bpfUpstream4Map, tcpKey, tcpValue, udpKey, udpValue);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)
+    public void testRefreshConntrackTimeout_Downstream4Map() throws Exception {
+        // TODO: Replace the dependencies BPF map with a non-mocked TestBpfMap object.
+        final TestBpfMap<Tether4Key, Tether4Value> bpfDownstream4Map =
+                new TestBpfMap<>(Tether4Key.class, Tether4Value.class);
+        doReturn(bpfDownstream4Map).when(mDeps).getBpfDownstream4Map();
+
+        final Tether4Key tcpKey = makeDownstream4Key(IPPROTO_TCP);
+        final Tether4Key udpKey = makeDownstream4Key(IPPROTO_UDP);
+        final Tether4Value tcpValue = makeDownstream4Value();
+        final Tether4Value udpValue = makeDownstream4Value();
+
+        checkRefreshConntrackTimeout(bpfDownstream4Map, tcpKey, tcpValue, udpKey, udpValue);
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
index d800816..fc34585 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
@@ -26,6 +26,7 @@
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
 
+import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
 import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_IFACE;
 import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_UID;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.ForwardedStats;
@@ -66,6 +67,7 @@
 import android.net.RouteInfo;
 import android.net.netstats.provider.NetworkStatsProvider;
 import android.net.util.SharedLog;
+import android.os.Build;
 import android.os.Handler;
 import android.os.test.TestLooper;
 import android.provider.Settings;
@@ -76,10 +78,13 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import com.android.testutils.TestableNetworkStatsProviderCbBinder;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -95,6 +100,9 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class OffloadControllerTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
     private static final String RNDIS0 = "test_rndis0";
     private static final String RMNET0 = "test_rmnet_data0";
     private static final String WLAN0 = "test_wlan0";
@@ -511,8 +519,8 @@
     public void testSetDataWarningAndLimit() throws Exception {
         // Verify the OffloadController is called by R framework, where the framework doesn't send
         // warning.
+        // R only uses HAL 1.0.
         checkSetDataWarningAndLimit(false, OFFLOAD_HAL_VERSION_1_0);
-        checkSetDataWarningAndLimit(false, OFFLOAD_HAL_VERSION_1_1);
         // Verify the OffloadController is called by S+ framework, where the framework sends
         // warning along with limit.
         checkSetDataWarningAndLimit(true, OFFLOAD_HAL_VERSION_1_0);
@@ -650,17 +658,26 @@
     }
 
     @Test
-    public void testDataWarningAndLimitCallback() throws Exception {
+    public void testDataWarningAndLimitCallback_LimitReached() throws Exception {
         enableOffload();
         startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/);
 
-        OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
+        final OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
         callback.onStoppedLimitReached();
         mTetherStatsProviderCb.expectNotifyStatsUpdated();
-        mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
 
+        if (isAtLeastS()) {
+            mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
+        } else {
+            mTetherStatsProviderCb.expectLegacyNotifyLimitReached();
+        }
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.R)  // HAL 1.1 is only supported from S
+    public void testDataWarningAndLimitCallback_WarningReached() throws Exception {
         startOffloadController(OFFLOAD_HAL_VERSION_1_1, true /*expectStart*/);
-        callback = mControlCallbackCaptor.getValue();
+        final OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
         callback.onWarningReached();
         mTetherStatsProviderCb.expectNotifyStatsUpdated();
         mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
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 b8389ea..e692015 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
@@ -20,6 +20,8 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 
+import static com.android.networkstack.apishim.common.ShimUtils.isAtLeastS;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.fail;
 
@@ -68,10 +70,10 @@
     public static final boolean BROADCAST_FIRST = false;
     public static final boolean CALLBACKS_FIRST = true;
 
-    final Map<NetworkCallback, NetworkCallbackInfo> mAllCallbacks = new ArrayMap<>();
+    final Map<NetworkCallback, Handler> 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, NetworkCallbackInfo> mTrackingDefault = new ArrayMap<>();
+    final Map<NetworkCallback, Handler> mTrackingDefault = new ArrayMap<>();
     final Map<NetworkCallback, NetworkRequestInfo> mListening = new ArrayMap<>();
     final Map<NetworkCallback, NetworkRequestInfo> mRequested = new ArrayMap<>();
     final Map<NetworkCallback, Integer> mLegacyTypeMap = new ArrayMap<>();
@@ -92,18 +94,12 @@
         mContext = ctx;
     }
 
-    static class NetworkCallbackInfo {
-        public final Handler handler;
-        NetworkCallbackInfo(Handler h) {
-            handler = h;
-        }
-    }
-
-    static class NetworkRequestInfo extends NetworkCallbackInfo {
+    static class NetworkRequestInfo {
         public final NetworkRequest request;
+        public final Handler handler;
         NetworkRequestInfo(NetworkRequest r, Handler h) {
-            super(h);
             request = r;
+            handler = h;
         }
     }
 
@@ -152,15 +148,15 @@
     private void sendDefaultNetworkCallbacks(TestNetworkAgent formerDefault,
             TestNetworkAgent defaultNetwork) {
         for (NetworkCallback cb : mTrackingDefault.keySet()) {
-            final NetworkCallbackInfo nri = mTrackingDefault.get(cb);
+            final Handler handler = mTrackingDefault.get(cb);
             if (defaultNetwork != null) {
-                nri.handler.post(() -> cb.onAvailable(defaultNetwork.networkId));
-                nri.handler.post(() -> cb.onCapabilitiesChanged(
+                handler.post(() -> cb.onAvailable(defaultNetwork.networkId));
+                handler.post(() -> cb.onCapabilitiesChanged(
                         defaultNetwork.networkId, defaultNetwork.networkCapabilities));
-                nri.handler.post(() -> cb.onLinkPropertiesChanged(
+                handler.post(() -> cb.onLinkPropertiesChanged(
                         defaultNetwork.networkId, defaultNetwork.linkProperties));
             } else if (formerDefault != null) {
-                nri.handler.post(() -> cb.onLost(formerDefault.networkId));
+                handler.post(() -> cb.onLost(formerDefault.networkId));
             }
         }
     }
@@ -201,10 +197,11 @@
         // 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)) {
-            registerSystemDefaultNetworkCallback(cb, h);
+            assertFalse(isAtLeastS());
+            addTrackDefaultCallback(cb, h);
         } else {
             assertFalse(mAllCallbacks.containsKey(cb));
-            mAllCallbacks.put(cb, new NetworkRequestInfo(req, h));
+            mAllCallbacks.put(cb, h);
             assertFalse(mRequested.containsKey(cb));
             mRequested.put(cb, new NetworkRequestInfo(req, h));
         }
@@ -213,10 +210,14 @@
     @Override
     public void registerSystemDefaultNetworkCallback(
             @NonNull NetworkCallback cb, @NonNull Handler h) {
+        addTrackDefaultCallback(cb, h);
+    }
+
+    private void addTrackDefaultCallback(@NonNull NetworkCallback cb, @NonNull Handler h) {
         assertFalse(mAllCallbacks.containsKey(cb));
-        mAllCallbacks.put(cb, new NetworkCallbackInfo(h));
+        mAllCallbacks.put(cb, h);
         assertFalse(mTrackingDefault.containsKey(cb));
-        mTrackingDefault.put(cb, new NetworkCallbackInfo(h));
+        mTrackingDefault.put(cb, h);
     }
 
     @Override
@@ -230,7 +231,7 @@
         assertFalse(mAllCallbacks.containsKey(cb));
         NetworkRequest newReq = new NetworkRequest(req.networkCapabilities, legacyType,
                 -1 /** testId */, req.type);
-        mAllCallbacks.put(cb, new NetworkRequestInfo(newReq, h));
+        mAllCallbacks.put(cb, h);
         assertFalse(mRequested.containsKey(cb));
         mRequested.put(cb, new NetworkRequestInfo(newReq, h));
         assertFalse(mLegacyTypeMap.containsKey(cb));
@@ -242,7 +243,7 @@
     @Override
     public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) {
         assertFalse(mAllCallbacks.containsKey(cb));
-        mAllCallbacks.put(cb, new NetworkRequestInfo(req, h));
+        mAllCallbacks.put(cb, h);
         assertFalse(mListening.containsKey(cb));
         mListening.put(cb, new NetworkRequestInfo(req, h));
     }
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 af28dd7..9e0c880 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -2610,12 +2610,57 @@
         reset(mBluetoothAdapter, mBluetoothPan);
     }
 
+    private void runDualStackUsbTethering(final String expectedIface) throws Exception {
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {expectedIface});
+        when(mRouterAdvertisementDaemon.start())
+                .thenReturn(true);
+        final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
+        runUsbTethering(upstreamState);
+
+        verify(mNetd).interfaceGetList();
+        verify(mNetd).tetherAddForward(expectedIface, TEST_MOBILE_IFNAME);
+        verify(mNetd).ipfwdAddInterfaceForward(expectedIface, 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();
+    }
+
+    private void forceUsbTetheringUse(final int function) {
+        Settings.Global.putInt(mContentResolver, TETHER_FORCE_USB_FUNCTIONS, function);
+        final ContentObserver observer = mTethering.getSettingsObserverForTest();
+        observer.onChange(false /* selfChange */);
+        mLooper.dispatchAll();
+    }
+
+    private void verifyUsbTetheringStopDueToSettingChange(final String iface) {
+        verify(mUsbManager, times(2)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+        mTethering.interfaceRemoved(iface);
+        sendUsbBroadcast(true, true, -1 /* no functions enabled */);
+        reset(mUsbManager, mNetd, mDhcpServer, mRouterAdvertisementDaemon,
+                mIPv6TetheringCoordinator, mDadProxy);
+    }
+
     @Test
-    public void testUsbTetheringWithNcmFunction() throws Exception {
-        when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
-                TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
+    public void testUsbFunctionConfigurationChange() throws Exception {
+        // Run TETHERING_NCM.
+        runNcmTethering();
+        verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+                any(), any());
+
+        // Change the USB tethering function to NCM. Because the USB tethering function was set to
+        // RNDIS (the default), tethering is stopped.
+        forceUsbTetheringUse(TETHER_USB_NCM_FUNCTION);
+        verifyUsbTetheringStopDueToSettingChange(TEST_NCM_IFNAME);
+
+        // TODO: move this into setup after allowing configure TEST_NCM_REGEX into
+        // config_tether_usb_regexs and config_tether_ncm_regexs at the same time.
         when(mResources.getStringArray(R.array.config_tether_usb_regexs))
-                .thenReturn(new String[] {TEST_NCM_REGEX});
+                .thenReturn(new String[] {TEST_RNDIS_REGEX, TEST_NCM_REGEX});
         sendConfigurationChanged();
 
         // If TETHERING_USB is forced to use ncm function, TETHERING_NCM would no longer be
@@ -2625,30 +2670,16 @@
         mLooper.dispatchAll();
         ncmResult.assertHasResult();
 
-        final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
-        runUsbTethering(upstreamState);
+        // Run TETHERING_USB with ncm configuration.
+        runDualStackUsbTethering(TEST_NCM_IFNAME);
 
-        verify(mNetd).interfaceGetList();
-        verify(mNetd).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
-        verify(mNetd).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+        // Change configuration to rndis.
+        forceUsbTetheringUse(TETHER_USB_RNDIS_FUNCTION);
+        verifyUsbTetheringStopDueToSettingChange(TEST_NCM_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 */);
+        // Run TETHERING_USB with rndis configuration.
+        runDualStackUsbTethering(TEST_RNDIS_IFNAME);
+        runStopUSBTethering();
     }
     // TODO: Test that a request for hotspot mode doesn't interfere with an
     // already operating tethering mode interface.
diff --git a/framework/src/android/net/util/MultinetworkPolicyTracker.java b/framework/src/android/net/util/MultinetworkPolicyTracker.java
index 7e62d28..9791cbf 100644
--- a/framework/src/android/net/util/MultinetworkPolicyTracker.java
+++ b/framework/src/android/net/util/MultinetworkPolicyTracker.java
@@ -182,7 +182,7 @@
     public void setTestAllowBadWifiUntil(long timeMs) {
         Log.d(TAG, "setTestAllowBadWifiUntil: " + mTestAllowBadWifiUntilMs);
         mTestAllowBadWifiUntilMs = timeMs;
-        updateAvoidBadWifi();
+        reevaluateInternal();
     }
 
     @VisibleForTesting
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 078a9eb..70ddb9a 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -107,4 +107,8 @@
     <string-array translatable="false" name="config_networkNotifySwitches">
     </string-array>
 
+    <!-- Whether to use an ongoing notification for signing in to captive portals, instead of a
+         notification that can be dismissed. -->
+    <bool name="config_ongoingSignInNotification">false</bool>
+
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index f0f4ae8..fd23566 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -30,6 +30,7 @@
             <item type="integer" name="config_networkWakeupPacketMask"/>
             <item type="integer" name="config_networkNotifySwitchType"/>
             <item type="array" name="config_networkNotifySwitches"/>
+            <item type="bool" name="config_ongoingSignInNotification"/>
 
         </policy>
     </overlayable>
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 05bacfa..de56789 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -6280,7 +6280,8 @@
                 callingAttributionTag);
         if (VDBG) log("pendingListenForNetwork for " + nri);
 
-        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri));
+        mHandler.sendMessage(mHandler.obtainMessage(
+                    EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT, nri));
     }
 
     /** Returns the next Network provider ID. */
@@ -7597,9 +7598,16 @@
             // 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());
+            // EXTRA_NETWORK_REQUEST is the active request, and whatever ID would be added would
+            // need to be sent as a separate extra.
+            final NetworkRequest req = nri.isMultilayerRequest()
+                    ? nri.getActiveRequest()
+                    // Non-multilayer listen requests do not have an active request
+                    : nri.mRequests.get(0);
+            if (req == null) {
+                Log.wtf(TAG, "No request in NRI " + nri);
+            }
+            intent.putExtra(ConnectivityManager.EXTRA_NETWORK_REQUEST, req);
             nri.mPendingIntentSent = true;
             sendIntent(nri.mPendingIntent, intent);
         }
diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
index b57ad5d..3dc79c5 100644
--- a/service/src/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
@@ -280,7 +280,11 @@
                 .setContentTitle(title)
                 .setContentIntent(intent)
                 .setLocalOnly(true)
-                .setOnlyAlertOnce(true);
+                .setOnlyAlertOnce(true)
+                // TODO: consider having action buttons to disconnect on the sign-in notification
+                // especially if it is ongoing
+                .setOngoing(notifyType == NotificationType.SIGN_IN
+                        && r.getBoolean(R.bool.config_ongoingSignInNotification));
 
         if (notifyType == NotificationType.NETWORK_SWITCH) {
             builder.setStyle(new Notification.BigTextStyle().bigText(details));
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
old mode 100644
new mode 100755
index 32e06e5..99118ac
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -230,11 +230,11 @@
             boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
 
             if (isNetwork || hasRestrictedPermission) {
-                Boolean permission = mApps.get(uid);
+                Boolean permission = mApps.get(UserHandle.getAppId(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);
+                    mApps.put(UserHandle.getAppId(uid), hasRestrictedPermission);
                 }
             }
 
@@ -325,14 +325,14 @@
         // 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);
+        return mApps.containsKey(UserHandle.getAppId(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));
+        return Boolean.TRUE.equals(mApps.get(UserHandle.getAppId(uid)));
     }
 
     private void update(Set<UserHandle> users, Map<Integer, Boolean> apps, boolean add) {
@@ -452,12 +452,13 @@
 
         // 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);
+        final int appId = UserHandle.getAppId(uid);
+        final Boolean permission = highestPermissionForUid(mApps.get(appId), packageName);
+        if (permission != mApps.get(appId)) {
+            mApps.put(appId, permission);
 
             Map<Integer, Boolean> apps = new HashMap<>();
-            apps.put(uid, permission);
+            apps.put(appId, permission);
             update(mUsers, apps, true);
         }
 
@@ -472,7 +473,7 @@
                 updateVpnUids(vpn.getKey(), changedUids, true);
             }
         }
-        mAllApps.add(UserHandle.getAppId(uid));
+        mAllApps.add(appId);
     }
 
     private Boolean highestUidNetworkPermission(int uid) {
@@ -529,16 +530,17 @@
             return;
         }
 
-        if (permission == mApps.get(uid)) {
+        final int appId = UserHandle.getAppId(uid);
+        if (permission == mApps.get(appId)) {
             // The permissions of this UID have not changed. Nothing to do.
             return;
         } else if (permission != null) {
-            mApps.put(uid, permission);
-            apps.put(uid, permission);
+            mApps.put(appId, permission);
+            apps.put(appId, permission);
             update(mUsers, apps, true);
         } else {
-            mApps.remove(uid);
-            apps.put(uid, NETWORK);  // doesn't matter which permission we pick here
+            mApps.remove(appId);
+            apps.put(appId, NETWORK);  // doesn't matter which permission we pick here
             update(mUsers, apps, false);
         }
     }
@@ -653,7 +655,7 @@
      */
     private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
         uids.remove(vpnAppUid);
-        uids.removeIf(uid -> mApps.getOrDefault(uid, NETWORK) == SYSTEM);
+        uids.removeIf(uid -> mApps.getOrDefault(UserHandle.getAppId(uid), NETWORK) == SYSTEM);
     }
 
     /**
@@ -795,12 +797,13 @@
         for (Integer uid : uidsToUpdate) {
             final Boolean permission = highestUidNetworkPermission(uid);
 
+            final int appId = UserHandle.getAppId(uid);
             if (null == permission) {
-                removedUids.put(uid, NETWORK); // Doesn't matter which permission is set here.
-                mApps.remove(uid);
+                removedUids.put(appId, NETWORK); // Doesn't matter which permission is set here.
+                mApps.remove(appId);
             } else {
-                updatedUids.put(uid, permission);
-                mApps.put(uid, permission);
+                updatedUids.put(appId, permission);
+                mApps.put(appId, permission);
             }
         }
 
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 6b149b6..e1fab09 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -66,10 +66,7 @@
     min_sdk_version: "30",
     // TODO: change to 31 as soon as it is available
     target_sdk_version: "30",
-    test_suites: ["device-tests", "mts"],
-    test_mainline_modules: [
-        "CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex",
-    ],
+    test_suites: ["general-tests", "mts"],
     defaults: [
         "framework-connectivity-test-defaults",
         "FrameworksNetTests-jni-defaults",
diff --git a/tests/common/AndroidTest_Coverage.xml b/tests/common/AndroidTest_Coverage.xml
index 577f36a..7c8e710 100644
--- a/tests/common/AndroidTest_Coverage.xml
+++ b/tests/common/AndroidTest_Coverage.xml
@@ -18,6 +18,7 @@
     </target_preparer>
 
     <option name="test-tag" value="ConnectivityCoverageTests" />
+    <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.connectivity.tests.coverage" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 3220565..ca066ea 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -55,16 +55,12 @@
 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;
@@ -133,9 +129,9 @@
 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.cts.util.CtsTetheringUtils;
 import android.net.util.KeepaliveUtils;
 import android.net.wifi.WifiManager;
 import android.os.Binder;
@@ -285,7 +281,6 @@
     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<>();
@@ -301,7 +296,6 @@
         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 */)) {
@@ -536,8 +530,10 @@
                     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 snapshotCapSpecifier =
+                    snapshot.getNetworkCapabilities().getNetworkSpecifier();
             final NetworkSpecifier redactedSnapshotCapSpecifier =
-                    snapshot.getNetworkCapabilities().getNetworkSpecifier().redact();
+                    snapshotCapSpecifier == null ? null : snapshotCapSpecifier.redact();
             assertEquals("", caps.describeImmutableDifferences(
                     snapshot.getNetworkCapabilities()
                             .setNetworkSpecifier(redactedSnapshotCapSpecifier)));
@@ -734,6 +730,8 @@
                     .isPrivateDnsBroken()) && networkForPrivateDns.equals(entry.getNetwork()));
         } finally {
             mCtsNetUtils.restorePrivateDnsSetting();
+            // Toggle wifi to make sure it is re-validated
+            reconnectWifi();
         }
     }
 
@@ -939,8 +937,8 @@
             // noticeably flaky.
             Thread.sleep(NO_CALLBACK_TIMEOUT_MS);
 
-            // TODO: BUG (b/189868426): this should also apply to listens
-            if (!useListen) {
+            // For R- frameworks, listens will receive duplicated callbacks. See b/189868426.
+            if (isAtLeastS() || !useListen) {
                 assertEquals("PendingIntent should only be received once", 1, receivedCount.get());
             }
         } finally {
@@ -953,12 +951,10 @@
 
     private void assertPendingIntentRequestMatches(NetworkRequest broadcasted, NetworkRequest filed,
             boolean useListen) {
-        // TODO: BUG (b/191713869): on S the request extra is null on listens
-        if (isAtLeastS() && useListen && broadcasted == null) return;
         assertArrayEquals(filed.networkCapabilities.getCapabilities(),
                 broadcasted.networkCapabilities.getCapabilities());
-        // TODO: BUG (b/189868426): this should also apply to listens
-        if (useListen) return;
+        // For R- frameworks, listens will receive duplicated callbacks. See b/189868426.
+        if (!isAtLeastS() && useListen) return;
         assertArrayEquals(filed.networkCapabilities.getTransportTypes(),
                 broadcasted.networkCapabilities.getTransportTypes());
     }
@@ -2232,14 +2228,15 @@
                 ConnectivitySettingsManager.getNetworkAvoidBadWifi(mContext);
         final int curPrivateDnsMode = ConnectivitySettingsManager.getPrivateDnsMode(mContext);
 
-        final TestTetheringEventCallback tetherEventCallback = new TestTetheringEventCallback();
+        TestTetheringEventCallback tetherEventCallback = null;
+        final CtsTetheringUtils tetherUtils = new CtsTetheringUtils(mContext);
         try {
-            mTm.registerTetheringEventCallback(c -> c.run() /* executor */, tetherEventCallback);
+            tetherEventCallback = tetherUtils.registerTetheringEventCallback();
             // Adopt for NETWORK_SETTINGS permission.
             mUiAutomation.adoptShellPermissionIdentity();
             // start tethering
             tetherEventCallback.assumeWifiTetheringSupported(mContext);
-            startWifiTethering(tetherEventCallback);
+            tetherUtils.startWifiTethering(tetherEventCallback);
             // Update setting to verify the behavior.
             mCm.setAirplaneMode(true);
             ConnectivitySettingsManager.setPrivateDnsMode(mContext,
@@ -2260,8 +2257,10 @@
             mCm.setAirplaneMode(false);
             ConnectivitySettingsManager.setNetworkAvoidBadWifi(mContext, curAvoidBadWifi);
             ConnectivitySettingsManager.setPrivateDnsMode(mContext, curPrivateDnsMode);
-            mTm.unregisterTetheringEventCallback(tetherEventCallback);
-            mTm.stopAllTethering();
+            if (tetherEventCallback != null) {
+                tetherUtils.unregisterTetheringEventCallback(tetherEventCallback);
+            }
+            tetherUtils.stopAllTethering();
             mUiAutomation.dropShellPermissionIdentity();
         }
     }
@@ -2308,19 +2307,6 @@
                 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,
diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
index c54ee91..7f710d7 100644
--- a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
@@ -23,6 +23,7 @@
 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.system.OsConstants.FIONREAD;
 
 import static org.junit.Assert.assertArrayEquals;
 
@@ -32,8 +33,10 @@
 import android.net.IpSecManager;
 import android.net.IpSecTransform;
 import android.platform.test.annotations.AppModeFull;
+import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
+import android.system.StructTimeval;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
@@ -46,15 +49,21 @@
 import org.junit.runner.RunWith;
 
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.net.DatagramPacket;
 import java.net.DatagramSocket;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
-import java.net.ServerSocket;
 import java.net.Socket;
+import java.net.SocketAddress;
 import java.net.SocketException;
+import java.net.SocketImpl;
+import java.net.SocketOptions;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
@@ -232,6 +241,12 @@
         public NativeTcpSocket(FileDescriptor fd) {
             super(fd);
         }
+
+        public JavaTcpSocket acceptToJavaSocket() throws Exception {
+            InetSocketAddress peer = new InetSocketAddress(0);
+            FileDescriptor newFd = Os.accept(mFd, peer);
+            return new JavaTcpSocket(new AcceptedTcpFileDescriptorSocket(newFd, peer, getPort()));
+        }
     }
 
     public static class NativeUdpSocket extends NativeSocket implements GenericUdpSocket {
@@ -357,6 +372,137 @@
         }
     }
 
+    private static class AcceptedTcpFileDescriptorSocket extends Socket {
+
+        AcceptedTcpFileDescriptorSocket(FileDescriptor fd, InetSocketAddress remote,
+                int localPort) throws IOException {
+            super(new FileDescriptorSocketImpl(fd, remote, localPort));
+            connect(remote);
+        }
+
+        private static class FileDescriptorSocketImpl extends SocketImpl {
+
+            private FileDescriptorSocketImpl(FileDescriptor fd, InetSocketAddress remote,
+                    int localPort) {
+                this.fd = fd;
+                this.address = remote.getAddress();
+                this.port = remote.getPort();
+                this.localport = localPort;
+            }
+
+            @Override
+            protected void create(boolean stream) throws IOException {
+                // The socket has been created.
+            }
+
+            @Override
+            protected void connect(String host, int port) throws IOException {
+                // The socket has connected.
+            }
+
+            @Override
+            protected void connect(InetAddress address, int port) throws IOException {
+                // The socket has connected.
+            }
+
+            @Override
+            protected void connect(SocketAddress address, int timeout) throws IOException {
+                // The socket has connected.
+            }
+
+            @Override
+            protected void bind(InetAddress host, int port) throws IOException {
+                // The socket is bounded.
+            }
+
+            @Override
+            protected void listen(int backlog) throws IOException {
+                throw new UnsupportedOperationException("listen");
+            }
+
+            @Override
+            protected void accept(SocketImpl s) throws IOException {
+                throw new UnsupportedOperationException("accept");
+            }
+
+            @Override
+            protected InputStream getInputStream() throws IOException {
+                return new FileInputStream(fd);
+            }
+
+            @Override
+            protected OutputStream getOutputStream() throws IOException {
+                return new FileOutputStream(fd);
+            }
+
+            @Override
+            protected int available() throws IOException {
+                try {
+                    return Os.ioctlInt(fd, FIONREAD);
+                } catch (ErrnoException e) {
+                    throw new IOException(e);
+                }
+            }
+
+            @Override
+            protected void close() throws IOException {
+                try {
+                    Os.close(fd);
+                } catch (ErrnoException e) {
+                    throw new IOException(e);
+                }
+            }
+
+            @Override
+            protected void sendUrgentData(int data) throws IOException {
+                throw new UnsupportedOperationException("sendUrgentData");
+            }
+
+            @Override
+            public void setOption(int optID, Object value) throws SocketException {
+                try {
+                    setOptionInternal(optID, value);
+                } catch (ErrnoException e) {
+                    throw new SocketException(e.getMessage());
+                }
+            }
+
+            private void setOptionInternal(int optID, Object value) throws ErrnoException,
+                    SocketException {
+                switch(optID) {
+                    case SocketOptions.SO_TIMEOUT:
+                        int millis = (Integer) value;
+                        StructTimeval tv = StructTimeval.fromMillis(millis);
+                        Os.setsockoptTimeval(fd, OsConstants.SOL_SOCKET, OsConstants.SO_RCVTIMEO,
+                                tv);
+                        return;
+                    default:
+                        throw new SocketException("Unknown socket option: " + optID);
+                }
+            }
+
+            @Override
+            public Object getOption(int optID) throws SocketException {
+                try {
+                    return getOptionInternal(optID);
+                } catch (ErrnoException e) {
+                    throw new SocketException(e.getMessage());
+                }
+            }
+
+            private Object getOptionInternal(int optID) throws ErrnoException, SocketException {
+                switch (optID) {
+                    case SocketOptions.SO_LINGER:
+                        // Returns an arbitrary value because IpSecManager doesn't actually
+                        // use this value.
+                        return 10;
+                    default:
+                        throw new SocketException("Unknown socket option: " + optID);
+                }
+            }
+        }
+    }
+
     public static class SocketPair<T> {
         public final T mLeftSock;
         public final T mRightSock;
@@ -441,8 +587,6 @@
     public static SocketPair<JavaTcpSocket> getJavaTcpSocketPair(
             InetAddress localAddr, IpSecManager ism, IpSecTransform transform) throws Exception {
         JavaTcpSocket clientSock = new JavaTcpSocket(new Socket());
-        ServerSocket serverSocket = new ServerSocket();
-        serverSocket.bind(new InetSocketAddress(localAddr, 0));
 
         // While technically the client socket does not need to be bound, the OpenJDK implementation
         // of Socket only allocates an FD when bind() or connect() or other similar methods are
@@ -451,16 +595,19 @@
         clientSock.mSocket.bind(new InetSocketAddress(localAddr, 0));
 
         // IpSecService doesn't support serverSockets at the moment; workaround using FD
-        FileDescriptor serverFd = serverSocket.getImpl().getFD$();
+        NativeTcpSocket server = new NativeTcpSocket(
+                Os.socket(getDomain(localAddr), OsConstants.SOCK_STREAM, OsConstants.IPPROTO_TCP));
+        Os.bind(server.mFd, localAddr, 0);
 
-        applyTransformBidirectionally(ism, transform, new NativeTcpSocket(serverFd));
+        applyTransformBidirectionally(ism, transform, server);
         applyTransformBidirectionally(ism, transform, clientSock);
 
-        clientSock.mSocket.connect(new InetSocketAddress(localAddr, serverSocket.getLocalPort()));
-        JavaTcpSocket acceptedSock = new JavaTcpSocket(serverSocket.accept());
+        Os.listen(server.mFd, 10 /* backlog */);
+        clientSock.mSocket.connect(new InetSocketAddress(localAddr, server.getPort()));
+        JavaTcpSocket acceptedSock = server.acceptToJavaSocket();
 
         applyTransformBidirectionally(ism, transform, acceptedSock);
-        serverSocket.close();
+        server.close();
 
         return new SocketPair<>(clientSock, acceptedSock);
     }
diff --git a/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt b/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt
new file mode 100644
index 0000000..8f17199
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt
@@ -0,0 +1,217 @@
+/*
+ * 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.content.Context
+import android.net.ConnectivityManager
+import android.net.LinkProperties
+import android.net.NetworkAgent
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.VpnManager
+import android.net.VpnTransportInfo
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.test.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.TestableNetworkCallback.HasNetwork
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// This test doesn't really have a constraint on how fast the methods should return. If it's
+// going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
+// without affecting the run time of successful runs. Thus, set a very high timeout.
+private const val TIMEOUT_MS = 30_000L
+// When waiting for a NetworkCallback to determine there was no timeout, waiting is the
+// only possible thing (the relevant handler is the one in the real ConnectivityService,
+// and then there is the Binder call), so have a short timeout for this as it will be
+// exhausted every time.
+private const val NO_CALLBACK_TIMEOUT = 200L
+
+private val testContext: Context
+    get() = InstrumentationRegistry.getContext()
+
+private fun score(exiting: Boolean = false, primary: Boolean = false) =
+        NetworkScore.Builder().setExiting(exiting).setTransportPrimary(primary)
+                // TODO : have a constant KEEP_CONNECTED_FOR_TEST ?
+                .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_FOR_HANDOVER)
+                .build()
+
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkScoreTest {
+    private val mCm = testContext.getSystemService(ConnectivityManager::class.java)
+    private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread")
+    private val mHandler by lazy { Handler(mHandlerThread.looper) }
+    private val agentsToCleanUp = mutableListOf<NetworkAgent>()
+    private val callbacksToCleanUp = mutableListOf<TestableNetworkCallback>()
+
+    @Before
+    fun setUp() {
+        mHandlerThread.start()
+    }
+
+    @After
+    fun tearDown() {
+        agentsToCleanUp.forEach { it.unregister() }
+        mHandlerThread.quitSafely()
+        callbacksToCleanUp.forEach { mCm.unregisterNetworkCallback(it) }
+    }
+
+    // Returns a networkCallback that sends onAvailable on the best network with TRANSPORT_TEST.
+    private fun makeTestNetworkCallback() = TestableNetworkCallback(TIMEOUT_MS).also { cb ->
+        mCm.registerBestMatchingNetworkCallback(NetworkRequest.Builder().clearCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_TEST).build(), cb, mHandler)
+        callbacksToCleanUp.add(cb)
+    }
+
+    // TestNetworkCallback is made to interact with a wrapper of NetworkAgent, because it's
+    // made for ConnectivityServiceTest.
+    // TODO : have TestNetworkCallback work for NetworkAgent too and remove this class.
+    private class AgentWrapper(val agent: NetworkAgent) : HasNetwork {
+        override val network = agent.network
+        fun sendNetworkScore(s: NetworkScore) = agent.sendNetworkScore(s)
+    }
+
+    private fun createTestNetworkAgent(
+            // The network always has TRANSPORT_TEST, plus optional transports
+        optionalTransports: IntArray = IntArray(size = 0),
+        everUserSelected: Boolean = false,
+        acceptUnvalidated: Boolean = false,
+        isExiting: Boolean = false,
+        isPrimary: Boolean = false
+    ): AgentWrapper {
+        val nc = NetworkCapabilities.Builder().apply {
+            addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+            optionalTransports.forEach { addTransportType(it) }
+            // Add capabilities that are common, just for realism. It's not strictly necessary
+            addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+            addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+            // Remove capabilities that a test network agent shouldn't have and that are not
+            // needed for the purposes of this test.
+            removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+            removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+            removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+            if (optionalTransports.contains(NetworkCapabilities.TRANSPORT_VPN)) {
+                addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+                removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                setTransportInfo(VpnTransportInfo(VpnManager.TYPE_VPN_SERVICE, null))
+            }
+            addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+        }.build()
+        val config = NetworkAgentConfig.Builder()
+                .setExplicitlySelected(everUserSelected)
+                .setUnvalidatedConnectivityAcceptable(acceptUnvalidated)
+                .build()
+        val score = score(exiting = isExiting, primary = isPrimary)
+        val context = testContext
+        val looper = mHandlerThread.looper
+        val agent = object : NetworkAgent(context, looper, "NetworkScore test agent", nc,
+                LinkProperties(), score, config, NetworkProvider(context, looper,
+                "NetworkScore test provider")) {}.also {
+            agentsToCleanUp.add(it)
+        }
+        runWithShellPermissionIdentity({ agent.register() }, MANAGE_TEST_NETWORKS)
+        agent.markConnected()
+        return AgentWrapper(agent)
+    }
+
+    @Test
+    fun testExitingLosesAndOldSatisfierWins() {
+        val cb = makeTestNetworkCallback()
+        val agent1 = createTestNetworkAgent()
+        cb.expectAvailableThenValidatedCallbacks(agent1)
+        val agent2 = createTestNetworkAgent()
+        // Because the existing network must win, the callback stays on agent1.
+        cb.assertNoCallback(NO_CALLBACK_TIMEOUT)
+        agent1.sendNetworkScore(score(exiting = true))
+        // Now that agent1 is exiting, the callback is satisfied by agent2.
+        cb.expectAvailableCallbacks(agent2.network)
+        agent1.sendNetworkScore(score(exiting = false))
+        // Agent1 is no longer exiting, but agent2 is the current satisfier.
+        cb.assertNoCallback(NO_CALLBACK_TIMEOUT)
+    }
+
+    @Test
+    fun testVpnWins() {
+        val cb = makeTestNetworkCallback()
+        val agent1 = createTestNetworkAgent()
+        cb.expectAvailableThenValidatedCallbacks(agent1.network)
+        val agent2 = createTestNetworkAgent(intArrayOf(NetworkCapabilities.TRANSPORT_VPN))
+        // VPN wins out against agent1 even before it's validated (hence the "then validated",
+        // because it becomes the best network for this callback before it validates)
+        cb.expectAvailableThenValidatedCallbacks(agent2.network)
+    }
+
+    @Test
+    fun testEverUserSelectedAcceptUnvalidatedWins() {
+        val cb = makeTestNetworkCallback()
+        val agent1 = createTestNetworkAgent()
+        cb.expectAvailableThenValidatedCallbacks(agent1.network)
+        val agent2 = createTestNetworkAgent(everUserSelected = true, acceptUnvalidated = true)
+        // agent2 wins out against agent1 even before it's validated, because user-selected and
+        // accept unvalidated networks should win against even networks that are validated.
+        cb.expectAvailableThenValidatedCallbacks(agent2.network)
+    }
+
+    @Test
+    fun testPreferredTransportOrder() {
+        val cb = makeTestNetworkCallback()
+        val agentCell = createTestNetworkAgent(intArrayOf(NetworkCapabilities.TRANSPORT_CELLULAR))
+        cb.expectAvailableThenValidatedCallbacks(agentCell.network)
+        val agentWifi = createTestNetworkAgent(intArrayOf(NetworkCapabilities.TRANSPORT_WIFI))
+        // In the absence of other discriminating factors, agentWifi wins against agentCell because
+        // of its better transport, but only after it validates.
+        cb.expectAvailableDoubleValidatedCallbacks(agentWifi)
+        val agentEth = createTestNetworkAgent(intArrayOf(NetworkCapabilities.TRANSPORT_ETHERNET))
+        // Likewise, agentEth wins against agentWifi after validation because of its better
+        // transport.
+        cb.expectAvailableCallbacksValidated(agentEth)
+    }
+
+    @Test
+    fun testTransportPrimary() {
+        val cb = makeTestNetworkCallback()
+        val agent1 = createTestNetworkAgent()
+        cb.expectAvailableThenValidatedCallbacks(agent1)
+        val agent2 = createTestNetworkAgent()
+        // Because the existing network must win, the callback stays on agent1.
+        cb.assertNoCallback(NO_CALLBACK_TIMEOUT)
+        agent2.sendNetworkScore(score(primary = true))
+        // Now that agent2 is primary, the callback is satisfied by agent2.
+        cb.expectAvailableCallbacks(agent2.network)
+        agent1.sendNetworkScore(score(primary = true))
+        // Agent1 is primary too, but agent2 is the current satisfier
+        cb.assertNoCallback(NO_CALLBACK_TIMEOUT)
+        agent2.sendNetworkScore(score(primary = false))
+        // Now agent1 is primary and agent2 isn't
+        cb.expectAvailableCallbacks(agent1.network)
+    }
+
+    // TODO (b/187929636) : add a test making sure that validated networks win over unvalidated
+    // ones. Right now this is not possible because this CTS can't directly manipulate the
+    // validation state of a network.
+}
diff --git a/tests/cts/net/src/android/net/ipv6/cts/PingTest.java b/tests/cts/net/src/android/net/ipv6/cts/PingTest.java
index 146fd83..8665fc8 100644
--- a/tests/cts/net/src/android/net/ipv6/cts/PingTest.java
+++ b/tests/cts/net/src/android/net/ipv6/cts/PingTest.java
@@ -115,7 +115,7 @@
 
         // Receive the response.
         if (useRecvfrom) {
-            InetSocketAddress from = new InetSocketAddress();
+            InetSocketAddress from = new InetSocketAddress(0);
             bytesRead = Os.recvfrom(s, responseBuffer, 0, from);
 
             // Check the source address and scope ID.
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 c220326..8c5372d 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
@@ -440,12 +440,6 @@
         return callback.getTetheringInterfaceRegexps().getTetherableWifiRegexs();
     }
 
-    public static boolean isWifiTetheringSupported(final Context ctx,
-            final TestTetheringEventCallback callback) throws Exception {
-        return !getWifiTetherableInterfaceRegexps(callback).isEmpty()
-                && isPortableHotspotSupported(ctx);
-    }
-
     /* Returns if wifi supports hotspot. */
     private static boolean isPortableHotspotSupported(final Context ctx) throws Exception {
         final PackageManager pm = ctx.getPackageManager();
@@ -522,4 +516,8 @@
         callback.expectNoTetheringActive();
         callback.expectOneOfOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
     }
+
+    public void stopAllTethering() {
+        mTm.stopAllTethering();
+    }
 }
diff --git a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
index be95b6c..08a3007 100644
--- a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
+++ b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
@@ -40,9 +40,10 @@
 import android.os.RemoteException;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -51,9 +52,9 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.invocation.InvocationOnMock;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkStatsManagerTest {
     private static final String TEST_SUBSCRIBER_ID = "subid";
 
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index b36e379..e7873af 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -59,7 +59,6 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.net.ConnectivityManager.NetworkCallback;
-import android.os.Build;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.Handler;
@@ -68,9 +67,10 @@
 import android.os.Messenger;
 import android.os.Process;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -79,9 +79,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
 public class ConnectivityManagerTest {
 
     @Mock Context mCtx;
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
index afd85e8..56e5c62 100644
--- a/tests/unit/java/android/net/Ikev2VpnProfileTest.java
+++ b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
@@ -26,13 +26,13 @@
 import android.os.Build;
 import android.test.mock.MockContext;
 
-import androidx.test.filters.SdkSuppress;
 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 com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -53,8 +53,8 @@
 
 /** Unit tests for {@link Ikev2VpnProfile.Builder}. */
 @SmallTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class Ikev2VpnProfileTest {
     private static final String SERVER_ADDR_STRING = "1.2.3.4";
     private static final String IDENTITY_STRING = "Identity";
diff --git a/tests/unit/java/android/net/IpMemoryStoreTest.java b/tests/unit/java/android/net/IpMemoryStoreTest.java
index 6be5396..0b82759 100644
--- a/tests/unit/java/android/net/IpMemoryStoreTest.java
+++ b/tests/unit/java/android/net/IpMemoryStoreTest.java
@@ -39,9 +39,10 @@
 import android.os.Build;
 import android.os.RemoteException;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -55,9 +56,9 @@
 import java.net.UnknownHostException;
 import java.util.Arrays;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class IpMemoryStoreTest {
     private static final String TAG = IpMemoryStoreTest.class.getSimpleName();
     private static final String TEST_CLIENT_ID = "testClientId";
diff --git a/tests/unit/java/android/net/IpSecAlgorithmTest.java b/tests/unit/java/android/net/IpSecAlgorithmTest.java
index fc08408..cac8c2d 100644
--- a/tests/unit/java/android/net/IpSecAlgorithmTest.java
+++ b/tests/unit/java/android/net/IpSecAlgorithmTest.java
@@ -28,11 +28,11 @@
 import android.os.Build;
 import android.os.Parcel;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.CollectionUtils;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -46,8 +46,8 @@
 
 /** Unit tests for {@link IpSecAlgorithm}. */
 @SmallTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class IpSecAlgorithmTest {
     private static final byte[] KEY_MATERIAL;
 
diff --git a/tests/unit/java/android/net/IpSecConfigTest.java b/tests/unit/java/android/net/IpSecConfigTest.java
index 457c923..b87cb48 100644
--- a/tests/unit/java/android/net/IpSecConfigTest.java
+++ b/tests/unit/java/android/net/IpSecConfigTest.java
@@ -25,17 +25,18 @@
 
 import android.os.Build;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 /** Unit tests for {@link IpSecConfig}. */
 @SmallTest
-@RunWith(JUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class IpSecConfigTest {
 
     @Test
diff --git a/tests/unit/java/android/net/IpSecManagerTest.java b/tests/unit/java/android/net/IpSecManagerTest.java
index 3fd0064..cda8eb7 100644
--- a/tests/unit/java/android/net/IpSecManagerTest.java
+++ b/tests/unit/java/android/net/IpSecManagerTest.java
@@ -35,11 +35,11 @@
 import android.system.Os;
 import android.test.mock.MockContext;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.IpSecService;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -51,8 +51,8 @@
 
 /** Unit tests for {@link IpSecManager}. */
 @SmallTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class IpSecManagerTest {
 
     private static final int TEST_UDP_ENCAP_PORT = 34567;
diff --git a/tests/unit/java/android/net/IpSecTransformTest.java b/tests/unit/java/android/net/IpSecTransformTest.java
index 96b09c3..81375f1 100644
--- a/tests/unit/java/android/net/IpSecTransformTest.java
+++ b/tests/unit/java/android/net/IpSecTransformTest.java
@@ -21,17 +21,18 @@
 
 import android.os.Build;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 /** Unit tests for {@link IpSecTransform}. */
 @SmallTest
-@RunWith(JUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class IpSecTransformTest {
 
     @Test
diff --git a/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
index 3fcb515..ed4f61d 100644
--- a/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
+++ b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java
@@ -27,19 +27,19 @@
 import android.os.Build;
 import android.util.Log;
 
-import androidx.test.filters.SdkSuppress;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
 
-@RunWith(JUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 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};
diff --git a/tests/unit/java/android/net/MacAddressTest.java b/tests/unit/java/android/net/MacAddressTest.java
index 0039e44..ae7deaa 100644
--- a/tests/unit/java/android/net/MacAddressTest.java
+++ b/tests/unit/java/android/net/MacAddressTest.java
@@ -24,11 +24,11 @@
 
 import android.os.Build;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.net.module.util.MacAddressUtils;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -38,8 +38,8 @@
 import java.util.Random;
 
 @SmallTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class MacAddressTest {
 
     static class AddrTypeTestCase {
diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt
index 4d04b19..f963593 100644
--- a/tests/unit/java/android/net/NetworkIdentityTest.kt
+++ b/tests/unit/java/android/net/NetworkIdentityTest.kt
@@ -21,14 +21,14 @@
 import android.net.NetworkIdentity.OEM_PRIVATE
 import android.net.NetworkIdentity.getOemBitfield
 import android.os.Build
-import androidx.test.filters.SdkSuppress
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import kotlin.test.assertEquals
 
-@RunWith(JUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 class NetworkIdentityTest {
     @Test
     fun testGetOemBitfield() {
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index 3ecce50..c5f8c00 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -42,11 +42,11 @@
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.tests.net.R;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.After;
 import org.junit.Test;
@@ -58,9 +58,9 @@
 import java.io.DataOutputStream;
 import java.util.Random;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkStatsHistoryTest {
     private static final String TAG = "NetworkStatsHistoryTest";
 
diff --git a/tests/unit/java/android/net/NetworkStatsTest.java b/tests/unit/java/android/net/NetworkStatsTest.java
index 9a3f4c2..1cdc6cb 100644
--- a/tests/unit/java/android/net/NetworkStatsTest.java
+++ b/tests/unit/java/android/net/NetworkStatsTest.java
@@ -43,9 +43,10 @@
 import android.os.Process;
 import android.util.ArrayMap;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import com.google.android.collect.Sets;
 
@@ -55,9 +56,9 @@
 import java.util.Arrays;
 import java.util.HashSet;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkStatsTest {
 
     private static final String TEST_IFACE = "test0";
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 437f961..49c7271 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -19,10 +19,10 @@
 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.SUBTYPE_COMBINED
 import android.net.NetworkIdentity.buildNetworkIdentity
 import android.net.NetworkStats.DEFAULT_NETWORK_ALL
 import android.net.NetworkStats.METERED_ALL
@@ -31,25 +31,25 @@
 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.WIFI_NETWORKID_ALL
 import android.net.NetworkTemplate.buildTemplateCarrierMetered
 import android.net.NetworkTemplate.buildTemplateMobileWithRatType
+import android.net.NetworkTemplate.buildTemplateWifi
+import android.net.NetworkTemplate.buildTemplateWifiWildcard
 import android.os.Build
 import android.telephony.TelephonyManager
-import androidx.test.filters.SdkSuppress
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
 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
@@ -62,8 +62,8 @@
 private const val TEST_SSID1 = "ssid1"
 private const val TEST_SSID2 = "ssid2"
 
-@RunWith(JUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 class NetworkTemplateTest {
     private val mockContext = mock(Context::class.java)
 
diff --git a/tests/unit/java/android/net/NetworkUtilsTest.java b/tests/unit/java/android/net/NetworkUtilsTest.java
index b292998..a28245d 100644
--- a/tests/unit/java/android/net/NetworkUtilsTest.java
+++ b/tests/unit/java/android/net/NetworkUtilsTest.java
@@ -20,8 +20,10 @@
 
 import android.os.Build;
 
-import androidx.test.filters.SdkSuppress;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -29,9 +31,9 @@
 import java.math.BigInteger;
 import java.util.TreeSet;
 
-@RunWith(AndroidJUnit4.class)
-@androidx.test.filters.SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkUtilsTest {
     @Test
     public void testRoutedIPv4AddressCount() {
diff --git a/tests/unit/java/android/net/QosSocketFilterTest.java b/tests/unit/java/android/net/QosSocketFilterTest.java
index 1635c34..91f2cdd 100644
--- a/tests/unit/java/android/net/QosSocketFilterTest.java
+++ b/tests/unit/java/android/net/QosSocketFilterTest.java
@@ -21,8 +21,10 @@
 
 import android.os.Build;
 
-import androidx.test.filters.SdkSuppress;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -30,9 +32,9 @@
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 
-@RunWith(AndroidJUnit4.class)
-@androidx.test.filters.SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class QosSocketFilterTest {
 
     @Test
diff --git a/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java b/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java
index e198c8b..ead964e 100644
--- a/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java
+++ b/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java
@@ -26,16 +26,17 @@
 import android.os.Build;
 import android.telephony.SubscriptionManager;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
+import com.android.testutils.DevSdkIgnoreRule;
+
 import org.junit.Test;
 
 /**
  * Unit test for {@link android.net.TelephonyNetworkSpecifier}.
  */
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class TelephonyNetworkSpecifierTest {
     private static final int TEST_SUBID = 5;
     private static final String TEST_SSID = "Test123";
diff --git a/tests/unit/java/android/net/VpnManagerTest.java b/tests/unit/java/android/net/VpnManagerTest.java
index b4d850a..532081a 100644
--- a/tests/unit/java/android/net/VpnManagerTest.java
+++ b/tests/unit/java/android/net/VpnManagerTest.java
@@ -31,12 +31,12 @@
 import android.test.mock.MockContext;
 import android.util.SparseArray;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.net.VpnProfile;
 import com.android.internal.util.MessageUtils;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -44,8 +44,8 @@
 
 /** Unit tests for {@link VpnManager}. */
 @SmallTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class VpnManagerTest {
     private static final String PKG_NAME = "fooPackage";
 
diff --git a/tests/unit/java/android/net/VpnTransportInfoTest.java b/tests/unit/java/android/net/VpnTransportInfoTest.java
index ae2ac04..b4c7ac4 100644
--- a/tests/unit/java/android/net/VpnTransportInfoTest.java
+++ b/tests/unit/java/android/net/VpnTransportInfoTest.java
@@ -26,16 +26,17 @@
 
 import android.os.Build;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class VpnTransportInfoTest {
 
     @Test
diff --git a/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java b/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java
index 219cfff..5d0b783 100644
--- a/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java
+++ b/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java
@@ -25,9 +25,10 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -38,9 +39,9 @@
 import java.util.Arrays;
 import java.util.Collections;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class ParcelableTests {
     @Test
     public void testNetworkAttributesParceling() throws Exception {
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 59a9316..31c8927 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -35,11 +35,11 @@
 import android.os.Message;
 import android.os.Messenger;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.AsyncChannel;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
 import org.junit.After;
@@ -49,9 +49,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NsdManagerTest {
 
     static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
index afe54d1..ca8cf07 100644
--- a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
@@ -26,9 +26,10 @@
 import android.os.Parcel;
 import android.os.StrictMode;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -38,9 +39,9 @@
 import java.util.Arrays;
 import java.util.Map;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NsdServiceInfoTest {
 
     public final static InetAddress LOCALHOST;
diff --git a/tests/unit/java/android/net/util/DnsUtilsTest.java b/tests/unit/java/android/net/util/DnsUtilsTest.java
index 0bac75e..660d516 100644
--- a/tests/unit/java/android/net/util/DnsUtilsTest.java
+++ b/tests/unit/java/android/net/util/DnsUtilsTest.java
@@ -27,9 +27,10 @@
 import android.net.InetAddresses;
 import android.os.Build;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,9 +40,9 @@
 import java.util.Collections;
 import java.util.List;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class DnsUtilsTest {
     private InetAddress stringToAddress(@NonNull String addr) {
         return InetAddresses.parseNumericAddress(addr);
diff --git a/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
index 65fb4ed..40f39a4 100644
--- a/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
+++ b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
@@ -26,16 +26,16 @@
 import android.net.NetworkCapabilities.TRANSPORT_VPN
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.os.Build
-import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import com.android.internal.R
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
 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
@@ -47,9 +47,9 @@
  * Build, install and run with:
  * atest android.net.util.KeepaliveUtilsTest
  */
-@RunWith(JUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 class KeepaliveUtilsTest {
 
     // Prepare mocked context with given resource strings.
diff --git a/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt b/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt
index 7d602ab..576b8d3 100644
--- a/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt
+++ b/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt
@@ -31,11 +31,11 @@
 import android.telephony.SubscriptionManager
 import android.telephony.TelephonyManager
 import android.test.mock.MockContentResolver
-import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
-import androidx.test.runner.AndroidJUnit4
 import com.android.connectivity.resources.R
 import com.android.internal.util.test.FakeSettingsProvider
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
 import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
@@ -59,9 +59,9 @@
  * Build, install and run with:
  * atest android.net.util.MultinetworkPolicyTrackerTest
  */
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 class MultinetworkPolicyTrackerTest {
     private val resources = mock(Resources::class.java).also {
         doReturn(R.integer.config_networkAvoidBadWifi).`when`(it).getIdentifier(
diff --git a/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java b/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java
index a4d8ea9..51388d4 100644
--- a/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java
+++ b/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java
@@ -31,17 +31,19 @@
 import android.system.ErrnoException;
 import android.system.Os;
 
-import androidx.test.filters.SdkSuppress;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import libcore.io.IoUtils;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-@RunWith(AndroidJUnit4.class)
-@androidx.test.filters.SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkUtilsInternalTest {
 
     private static void expectSocketSuccess(String msg, int domain, int type) {
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
index 64cbc4e..a945a1f 100644
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
@@ -27,12 +27,13 @@
 import android.net.IpSecAlgorithm;
 import android.os.Build;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -40,8 +41,8 @@
 
 /** Unit tests for {@link VpnProfile}. */
 @SmallTest
-@RunWith(JUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class VpnProfileTest {
     private static final String DUMMY_PROFILE_KEY = "Test";
 
diff --git a/tests/unit/java/com/android/internal/util/BitUtilsTest.java b/tests/unit/java/com/android/internal/util/BitUtilsTest.java
index 9c6ac9b..aab1268 100644
--- a/tests/unit/java/com/android/internal/util/BitUtilsTest.java
+++ b/tests/unit/java/com/android/internal/util/BitUtilsTest.java
@@ -32,9 +32,10 @@
 
 import android.os.Build;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -44,8 +45,8 @@
 import java.util.Random;
 
 @SmallTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class BitUtilsTest {
 
     @Test
diff --git a/tests/unit/java/com/android/internal/util/RingBufferTest.java b/tests/unit/java/com/android/internal/util/RingBufferTest.java
index 81a6513..13cf840 100644
--- a/tests/unit/java/com/android/internal/util/RingBufferTest.java
+++ b/tests/unit/java/com/android/internal/util/RingBufferTest.java
@@ -22,16 +22,17 @@
 
 import android.os.Build;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @SmallTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class RingBufferTest {
 
     @Test
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 0e67583..53c4695 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -300,9 +300,7 @@
 import android.util.SparseArray;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.connectivity.resources.R;
 import com.android.internal.app.IBatteryStats;
@@ -326,6 +324,8 @@
 import com.android.server.connectivity.Vpn;
 import com.android.server.connectivity.VpnProfileStore;
 import com.android.server.net.NetworkPinner;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.ExceptionUtils;
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.RecorderCallback.CallbackEntry;
@@ -389,9 +389,9 @@
  * Build, install and run with:
  *  runtest frameworks-net -c com.android.server.ConnectivityServiceTest
  */
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class ConnectivityServiceTest {
     private static final String TAG = "ConnectivityServiceTest";
 
@@ -5747,37 +5747,59 @@
     @Test
     public void testNetworkCallbackMaximum() throws Exception {
         final int MAX_REQUESTS = 100;
-        final int CALLBACKS = 89;
-        final int INTENTS = 11;
+        final int CALLBACKS = 87;
+        final int DIFF_INTENTS = 10;
+        final int SAME_INTENTS = 10;
         final int SYSTEM_ONLY_MAX_REQUESTS = 250;
-        assertEquals(MAX_REQUESTS, CALLBACKS + INTENTS);
+        // Assert 1 (Default request filed before testing) + CALLBACKS + DIFF_INTENTS +
+        // 1 (same intent) = MAX_REQUESTS - 1, since the capacity is MAX_REQUEST - 1.
+        assertEquals(MAX_REQUESTS - 1, 1 + CALLBACKS + DIFF_INTENTS + 1);
 
         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);
+        for (int j = 0; j < CALLBACKS; j++) {
+            final NetworkCallback cb = new NetworkCallback();
+            if (j < CALLBACKS / 2) {
+                mCm.requestNetwork(networkRequest, cb);
+            } else {
+                mCm.registerNetworkCallback(networkRequest, cb);
+            }
             registered.add(cb);
         }
-        while (j++ < CALLBACKS) {
-            NetworkCallback cb = new NetworkCallback();
-            mCm.registerNetworkCallback(networkRequest, cb);
-            registered.add(cb);
+
+        // Since ConnectivityService will de-duplicate the request with the same intent,
+        // register multiple times does not really increase multiple requests.
+        final PendingIntent same_pi = PendingIntent.getBroadcast(mContext, 0 /* requestCode */,
+                new Intent("same"), FLAG_IMMUTABLE);
+        for (int j = 0; j < SAME_INTENTS; j++) {
+            mCm.registerNetworkCallback(networkRequest, same_pi);
+            // Wait for the requests with the same intent to be de-duplicated. Because
+            // ConnectivityService side incrementCountOrThrow in binder, decrementCount in handler
+            // thread, waitForIdle is needed to ensure decrementCount being invoked for same intent
+            // requests before doing further tests.
+            waitForIdle();
         }
-        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);
+        for (int j = 0; j < SAME_INTENTS; j++) {
+            mCm.requestNetwork(networkRequest, same_pi);
+            // Wait for the requests with the same intent to be de-duplicated.
+            // Refer to the reason above.
+            waitForIdle();
         }
-        while (j++ < INTENTS) {
-            final PendingIntent pi = PendingIntent.getBroadcast(mContext, 0 /* requestCode */,
-                    new Intent("b" + j), FLAG_IMMUTABLE);
-            mCm.registerNetworkCallback(networkRequest, pi);
-            registered.add(pi);
+        registered.add(same_pi);
+
+        for (int j = 0; j < DIFF_INTENTS; j++) {
+            if (j < DIFF_INTENTS / 2) {
+                final PendingIntent pi = PendingIntent.getBroadcast(mContext, 0 /* requestCode */,
+                        new Intent("a" + j), FLAG_IMMUTABLE);
+                mCm.requestNetwork(networkRequest, pi);
+                registered.add(pi);
+            } else {
+                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.
@@ -5827,10 +5849,10 @@
 
         for (Object o : registered) {
             if (o instanceof NetworkCallback) {
-                mCm.unregisterNetworkCallback((NetworkCallback)o);
+                mCm.unregisterNetworkCallback((NetworkCallback) o);
             }
             if (o instanceof PendingIntent) {
-                mCm.unregisterNetworkCallback((PendingIntent)o);
+                mCm.unregisterNetworkCallback((PendingIntent) o);
             }
         }
         waitForIdle();
diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
index 2b5bfac..5bbbe40 100644
--- a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
@@ -64,13 +64,14 @@
 import android.test.mock.MockContext;
 import android.util.ArraySet;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
 import com.android.server.IpSecService.TunnelInterfaceRecord;
+import com.android.testutils.DevSdkIgnoreRule;
 
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -84,8 +85,10 @@
 /** Unit tests for {@link IpSecService}. */
 @SmallTest
 @RunWith(Parameterized.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
 public class IpSecServiceParameterizedTest {
+    @Rule
+    public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(
+            Build.VERSION_CODES.R /* ignoreClassUpTo */);
 
     private static final int TEST_SPI = 0xD1201D;
 
diff --git a/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
index 0e3b03c..6957d51 100644
--- a/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
@@ -34,12 +34,12 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.IpSecService.IResource;
 import com.android.server.IpSecService.RefcountedResource;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -53,8 +53,8 @@
 
 /** Unit tests for {@link IpSecService.RefcountedResource}. */
 @SmallTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class IpSecServiceRefcountedResourceTest {
     Context mMockContext;
     IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig;
diff --git a/tests/unit/java/com/android/server/IpSecServiceTest.java b/tests/unit/java/com/android/server/IpSecServiceTest.java
index 2dc21c0..fabd6f1 100644
--- a/tests/unit/java/com/android/server/IpSecServiceTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceTest.java
@@ -51,9 +51,10 @@
 import android.system.StructStat;
 import android.util.Range;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import dalvik.system.SocketTagger;
 
@@ -72,8 +73,8 @@
 
 /** Unit tests for {@link IpSecService}. */
 @SmallTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class IpSecServiceTest {
 
     private static final int DROID_SPI = 0xD1201D;
diff --git a/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt b/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt
index 750703c..64736f2 100644
--- a/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt
+++ b/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt
@@ -45,11 +45,11 @@
 import android.net.NetworkInfo.DetailedState.DISCONNECTED
 import android.os.Build
 import android.telephony.TelephonyManager
-import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
-import androidx.test.runner.AndroidJUnit4
 import com.android.server.ConnectivityService.LegacyTypeTracker
 import com.android.server.connectivity.NetworkAgentInfo
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertSame
@@ -66,9 +66,9 @@
 
 const val UNSUPPORTED_TYPE = TYPE_WIMAX
 
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 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,
diff --git a/tests/unit/java/com/android/server/NetIdManagerTest.kt b/tests/unit/java/com/android/server/NetIdManagerTest.kt
index 5c43197..811134e 100644
--- a/tests/unit/java/com/android/server/NetIdManagerTest.kt
+++ b/tests/unit/java/com/android/server/NetIdManagerTest.kt
@@ -17,19 +17,19 @@
 package com.android.server
 
 import android.os.Build
-import androidx.test.filters.SdkSuppress
 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.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.ExceptionUtils.ThrowingRunnable
+import com.android.testutils.assertThrows
 import org.junit.Test
 import org.junit.runner.RunWith
 import kotlin.test.assertEquals
 
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 class NetIdManagerTest {
     @Test
     fun testReserveReleaseNetId() {
diff --git a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
index 32a8f3b..ea29da0 100644
--- a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
+++ b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
@@ -45,12 +45,11 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.ArrayMap;
 
-import androidx.test.filters.SdkSuppress;
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.internal.app.IBatteryStats;
 import com.android.server.NetworkManagementService.Dependencies;
 import com.android.server.net.BaseNetworkObserver;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.After;
 import org.junit.Before;
@@ -66,9 +65,9 @@
 /**
  * Tests for {@link NetworkManagementService}.
  */
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkManagementServiceTest {
     private NetworkManagementService mNMService;
     @Mock private Context mContext;
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 5ea0e8e..e80a938 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -37,13 +37,13 @@
 import android.os.Looper;
 import android.os.Message;
 
-import androidx.test.filters.SdkSuppress;
 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 com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
 import org.junit.After;
@@ -58,9 +58,9 @@
 // TODOs:
 //  - test client can send requests and receive replies
 //  - test NSD_ON ENABLE/DISABLED listening
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NsdServiceTest {
 
     static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index 9f0c9d6..9ef558f 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -60,12 +60,12 @@
 import android.test.mock.MockContentResolver;
 import android.util.SparseArray;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.MessageUtils;
 import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import libcore.net.InetAddressUtils;
 
@@ -85,9 +85,9 @@
  * Build, install and run with:
  *  runtest frameworks-net -c com.android.server.connectivity.DnsManagerTest
  */
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class DnsManagerTest {
     static final String TEST_IFACENAME = "test_wlan0";
     static final int TEST_NETID = 100;
diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
index 02f3da7..785153a 100644
--- a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
@@ -22,27 +22,25 @@
 import android.os.Build
 import android.text.TextUtils
 import android.util.ArraySet
-import androidx.test.filters.SdkSuppress
 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 com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
 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)
+@RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 class FullScoreTest {
     // Convenience methods
     fun FullScore.withPolicies(
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
index d6acea1..52b05aa 100644
--- a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
@@ -45,10 +45,9 @@
 import android.os.Build;
 import android.test.suitebuilder.annotation.SmallTest;
 
-import androidx.test.filters.SdkSuppress;
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -57,9 +56,9 @@
 import java.util.List;
 
 // TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto.
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class IpConnectivityEventBuilderTest {
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
index d0038a4..063ccd3 100644
--- a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
+++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java
@@ -51,11 +51,10 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Base64;
 
-import androidx.test.filters.SdkSuppress;
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.internal.util.BitUtils;
 import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -67,9 +66,9 @@
 import java.io.PrintWriter;
 import java.io.StringWriter;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class IpConnectivityMetricsTest {
     static final IpReachabilityEvent FAKE_EV =
             new IpReachabilityEvent(IpReachabilityEvent.NUD_FAILED);
diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
index 3f3bfdd..58a7c89 100644
--- a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -46,13 +46,13 @@
 import android.os.Build;
 import android.text.format.DateUtils;
 
-import androidx.test.filters.SdkSuppress;
 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 com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.After;
 import org.junit.Before;
@@ -61,9 +61,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class LingerMonitorTest {
     static final String CELLULAR = "CELLULAR";
     static final String WIFI     = "WIFI";
diff --git a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
index f8ef31c..e2ad00d 100644
--- a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
@@ -28,8 +28,7 @@
 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.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
@@ -63,15 +62,15 @@
 import android.util.DataUnit;
 import android.util.RecurrenceRule;
 
-import androidx.test.filters.SdkSuppress;
 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 com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.After;
 import org.junit.Before;
@@ -89,9 +88,9 @@
 import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class MultipathPolicyTrackerTest {
     private static final Network TEST_NETWORK = new Network(123);
     private static final int POLICY_SNOOZED = -100;
diff --git a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
index 9204d14..f358726 100644
--- a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
+++ b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
@@ -44,11 +44,11 @@
 import android.os.Handler;
 import android.os.test.TestLooper;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.ConnectivityService;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -58,9 +58,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class Nat464XlatTest {
 
     static final String BASE_IFACE = "test0";
diff --git a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
index 8aad1a2..7d6c3ae 100644
--- a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
@@ -36,11 +36,10 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Base64;
 
-import androidx.test.filters.SdkSuppress;
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
 import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -54,9 +53,9 @@
 import java.util.Comparator;
 import java.util.List;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetdEventListenerServiceTest {
     private static final String EXAMPLE_IPV4 = "192.0.2.1";
     private static final String EXAMPLE_IPV6 = "2001:db8:1200::2:1";
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
index 160068f..c924535 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -16,8 +16,16 @@
 
 package com.android.server.connectivity;
 
-import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.*;
+import static android.app.Notification.FLAG_ONGOING_EVENT;
 
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.LOST_INTERNET;
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.NETWORK_SWITCH;
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.NO_INTERNET;
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.PARTIAL_CONNECTIVITY;
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.PRIVATE_DNS_BROKEN;
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.SIGN_IN;
+
+import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.clearInvocations;
@@ -43,12 +51,12 @@
 import android.os.UserHandle;
 import android.telephony.TelephonyManager;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.connectivity.resources.R;
 import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.After;
 import org.junit.Before;
@@ -65,9 +73,9 @@
 import java.util.Collections;
 import java.util.List;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkNotificationManagerTest {
 
     private static final String TEST_SSID = "Test SSID";
@@ -229,19 +237,47 @@
         verify(mNotificationManager, never()).notify(any(), anyInt(), any());
     }
 
+    private void assertNotification(NotificationType type, boolean ongoing) {
+        final int id = 101;
+        final String tag = NetworkNotificationManager.tagFor(id);
+        final ArgumentCaptor<Notification> noteCaptor = ArgumentCaptor.forClass(Notification.class);
+        mManager.showNotification(id, type, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, times(1)).notify(eq(tag), eq(type.eventId),
+                noteCaptor.capture());
+
+        assertEquals("Notification ongoing flag should be " + (ongoing ? "set" : "unset"),
+                ongoing, (noteCaptor.getValue().flags & FLAG_ONGOING_EVENT) != 0);
+    }
+
     @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());
+        assertNotification(NO_INTERNET, false /* ongoing */);
 
         // Captive portal detection triggers SIGN_IN a bit later, clearing the previous NO_INTERNET
-        mManager.showNotification(id, SIGN_IN, mWifiNai, mCellNai, null, false);
+        assertNotification(SIGN_IN, false /* ongoing */);
         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 testOngoingSignInNotification() {
+        doReturn(true).when(mResources).getBoolean(R.bool.config_ongoingSignInNotification);
+        final int id = 101;
+        final String tag = NetworkNotificationManager.tagFor(id);
+
+        // Show first NO_INTERNET
+        assertNotification(NO_INTERNET, false /* ongoing */);
+
+        // Captive portal detection triggers SIGN_IN a bit later, clearing the previous NO_INTERNET
+        assertNotification(SIGN_IN, true /* ongoing */);
+        verify(mNotificationManager, times(1)).cancel(eq(tag), eq(NO_INTERNET.eventId));
 
         // Network disconnects
         mManager.clearNotification(id);
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt
index d12f1c0..d03c567 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt
@@ -21,9 +21,9 @@
 import android.net.NetworkRequest
 import android.net.NetworkScore.KEEP_CONNECTED_NONE
 import android.os.Build
-import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
-import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.eq
@@ -34,9 +34,9 @@
 
 const val POLICY_NONE = 0L
 
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 class NetworkOfferTest {
     val mockCallback = mock(INetworkOfferCallback::class.java)
 
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
index c35b60e..4408958 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
@@ -24,11 +24,11 @@
 import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY
 import android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI
 import android.os.Build
-import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
-import androidx.test.runner.AndroidJUnit4
 import com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD
 import com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
 import org.junit.Test
 import org.junit.runner.RunWith
 import kotlin.test.assertEquals
@@ -38,8 +38,8 @@
 private fun caps(transport: Int) = NetworkCapabilities.Builder().addTransportType(transport).build()
 
 @SmallTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 class NetworkRankerTest {
     private val mRanker = NetworkRanker()
 
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index 7c733da..7f923d6 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -79,9 +79,10 @@
 import android.util.SparseIntArray;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -99,9 +100,9 @@
 import java.util.HashSet;
 import java.util.Set;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class PermissionMonitorTest {
     private static final UserHandle MOCK_USER1 = UserHandle.of(0);
     private static final UserHandle MOCK_USER2 = UserHandle.of(1);
@@ -528,13 +529,13 @@
         // 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));
+        doReturn(SYSTEM).when(mPermissionMonitor).highestPermissionForUid(eq(SYSTEM), anyString());
 
         // Add SYSTEM_PACKAGE2, expect only have network permission.
         mPermissionMonitor.onUserAdded(MOCK_USER1);
@@ -549,6 +550,21 @@
         netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2},
                 new int[]{SYSTEM_UID});
 
+        // Remove SYSTEM_PACKAGE2, expect keep system permission.
+        when(mPackageManager.getPackagesForUid(MOCK_USER1.getUid(SYSTEM_UID)))
+                .thenReturn(new String[]{SYSTEM_PACKAGE1});
+        when(mPackageManager.getPackagesForUid(MOCK_USER2.getUid(SYSTEM_UID)))
+                .thenReturn(new String[]{SYSTEM_PACKAGE1});
+        removePackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2},
+                SYSTEM_PACKAGE2, SYSTEM_UID);
+        netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2},
+                new int[]{SYSTEM_UID});
+
+        // Add SYSTEM_PACKAGE2, expect keep system permission.
+        addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, SYSTEM_PACKAGE2, SYSTEM_UID);
+        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});
@@ -556,6 +572,10 @@
                 new int[]{MOCK_UID1});
 
         // Remove MOCK_UID1, expect no permission left for all user.
+        when(mPackageManager.getPackagesForUid(MOCK_USER1.getUid(MOCK_UID1)))
+                .thenReturn(new String[]{});
+        when(mPackageManager.getPackagesForUid(MOCK_USER2.getUid(MOCK_UID1)))
+                .thenReturn(new String[]{});
         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},
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 6971b3ca..b706090 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -82,7 +82,6 @@
 import android.net.VpnTransportInfo;
 import android.net.ipsec.ike.IkeSessionCallback;
 import android.net.ipsec.ike.exceptions.IkeProtocolException;
-import android.os.Build;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.ConditionVariable;
@@ -97,15 +96,15 @@
 import android.util.ArraySet;
 import android.util.Range;
 
-import androidx.test.filters.SdkSuppress;
 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 com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -140,9 +139,9 @@
  * Build, install and run with:
  *  runtest frameworks-net -c com.android.server.connectivity.VpnTest
  */
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
 public class VpnTest {
     private static final String TAG = "VpnTest";
 
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java b/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java
index 84a1a8f..03d9404 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java
@@ -28,11 +28,11 @@
 import android.os.Build;
 import android.telephony.TelephonyManager;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.LocalServices;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.After;
 import org.junit.Before;
@@ -41,9 +41,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkStatsAccessTest {
     private static final String TEST_PKG = "com.example.test";
     private static final int TEST_UID = 12345;
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.java b/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.java
index 57f48f5..e771558 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.java
@@ -51,11 +51,11 @@
 import android.util.RecurrenceRule;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.tests.net.R;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import libcore.io.IoUtils;
 import libcore.io.Streams;
@@ -81,9 +81,9 @@
 /**
  * Tests for {@link NetworkStatsCollection}.
  */
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkStatsCollectionTest {
 
     private static final String TEST_FILE = "test.bin";
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
index 4c80678..8d7aa4e 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -40,11 +40,11 @@
 import android.os.Build;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.tests.net.R;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import libcore.io.IoUtils;
 import libcore.io.Streams;
@@ -62,9 +62,9 @@
 import java.io.OutputStream;
 
 /** Tests for {@link NetworkStatsFactory}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkStatsFactoryTest extends NetworkStatsBaseTest {
     private static final String CLAT_PREFIX = "v4-";
 
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 7e8081b..e35104e 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -51,11 +51,11 @@
 import android.telephony.TelephonyManager;
 import android.util.ArrayMap;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.net.NetworkStatsServiceTest.LatchedHandler;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
 import org.junit.Before;
@@ -70,9 +70,9 @@
 /**
  * Tests for {@link NetworkStatsObservers}.
  */
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkStatsObserversTest {
     private static final String TEST_IFACE = "test0";
     private static final String TEST_IFACE2 = "test1";
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 9b2c278..ab76460 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -114,14 +114,14 @@
 
 import androidx.annotation.Nullable;
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SdkSuppress;
 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.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 import com.android.testutils.TestableNetworkStatsProviderBinder;
 
@@ -148,9 +148,9 @@
  * 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)
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
     private static final String TAG = "NetworkStatsServiceTest";
 
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
index f30a9c5..2bc385c 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
@@ -42,15 +42,14 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 
-import androidx.test.filters.SdkSuppress;
-
 import com.android.internal.util.CollectionUtils;
 import com.android.server.net.NetworkStatsSubscriptionsMonitor.RatTypeListener;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 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;
@@ -60,8 +59,8 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
-@RunWith(JUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public final class NetworkStatsSubscriptionsMonitorTest {
     private static final int TEST_SUBID1 = 3;
     private static final int TEST_SUBID2 = 5;
diff --git a/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java b/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java
index 406fdd8..5f3efed 100644
--- a/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java
+++ b/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java
@@ -22,9 +22,10 @@
 import android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirk;
 import android.os.Build;
 
-import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -36,8 +37,8 @@
 
 /** Unit tests for {@link NetworkAttributes}. */
 @SmallTest
-@RunWith(AndroidJUnit4.class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
 public class NetworkAttributesTest {
     private static final String WEIGHT_FIELD_NAME_PREFIX = "WEIGHT_";
     private static final float EPSILON = 0.0001f;