Snap for 13219462 from 3a072625c7d80a27fdd01a0679e5d970704d51a5 to 25Q2-release

Change-Id: I2d5a5beaf43f5ea612cf5d076d1603a53f656e85
diff --git a/OWNERS_core_networking_xts b/OWNERS_core_networking_xts
index 9e4e4a1..60ca885 100644
--- a/OWNERS_core_networking_xts
+++ b/OWNERS_core_networking_xts
@@ -10,3 +10,5 @@
 # In addition to cherry-picks, flaky test fixes and no-op refactors, also for
 # NsdManager tests
 reminv@google.com #{LAST_RESORT_SUGGESTION}
+# Only for APF firmware tests (to verify correct behaviour of the wifi APF interpreter)
+yuyanghuang@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 2878f79..531489d 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -47,15 +47,6 @@
 // as the above target may have different "enabled" values
 // depending on the branch
 
-apex_defaults {
-    name: "CronetInTetheringApexDefaults",
-    jni_libs: [
-        "cronet_aml_components_cronet_android_cronet",
-        "//external/cronet/third_party/boringssl:libcrypto",
-        "//external/cronet/third_party/boringssl:libssl",
-    ],
-}
-
 apex {
     name: "com.android.tethering",
     defaults: [
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 2f9c3bc..0a66f01 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -37,7 +37,6 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
-import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
@@ -1259,44 +1258,6 @@
             return sj.toString();
         }
 
-        @SuppressLint("UnflaggedApi")
-        private static boolean supportsInterfaceName(int tetheringType) {
-            // TODO: Check the interface name for TETHERING_WIFI and TETHERING_WIFI_P2P once
-            //       they're actually used.
-            // Suppress lint for TETHERING_VIRTUAL since this method is only used internally.
-            return tetheringType == TETHERING_VIRTUAL;
-        }
-
-        private static boolean supportsConcurrentConnectivityScopes(int tetheringType) {
-            // Currently, only WIFI supports simultaneous local and global connectivity.
-            // This can't happen for REQUEST_TYPE_EXPLICIT requests, because
-            // TetheringRequest.Builder will not allow building an explicit TetheringRequest
-            // with TETHERING_WIFI and CONNECTIVITY_SCOPE_LOCAL, but when local-only hotspot
-            // is running, there is a REQUEST_TYPE_IMPLICIT request in the serving request list.
-            return tetheringType == TETHERING_WIFI;
-        }
-
-        /**
-         * Returns true if the other TetheringRequest "fuzzy" matches this one. This is used
-         * internally to match tracked requests with external requests from API calls, and to reject
-         * additional requests that the link layer has no capacity for.
-         * @hide
-         */
-        public boolean fuzzyMatches(final TetheringRequest other) {
-            if (other == null) return false;
-            final int type = getTetheringType();
-            if (type != other.getTetheringType()) return false;
-            if (supportsInterfaceName(type)
-                    && !TextUtils.equals(getInterfaceName(), other.getInterfaceName())) {
-                return false;
-            }
-            if (supportsConcurrentConnectivityScopes(type)
-                    && getConnectivityScope() != other.getConnectivityScope()) {
-                return false;
-            }
-            return true;
-        }
-
         /**
          * @hide
          */
@@ -1318,8 +1279,6 @@
         public boolean equalsIgnoreUidPackage(TetheringRequest otherRequest) {
             TetheringRequestParcel parcel = getParcel();
             TetheringRequestParcel otherParcel = otherRequest.getParcel();
-            // Note: Changes here should also be reflected in fuzzyMatches(TetheringRequest) when
-            //       appropriate.
             return parcel.requestType == otherParcel.requestType
                     && parcel.tetheringType == otherParcel.tetheringType
                     && Objects.equals(parcel.localIPv4Address, otherParcel.localIPv4Address)
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 70a3442..d0ba431 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -415,6 +415,12 @@
         return mIpv4PrefixRequest;
     }
 
+    /** The TetheringRequest the IpServer started with. */
+    @Nullable
+    public TetheringRequest getTetheringRequest() {
+        return mTetheringRequest;
+    }
+
     /**
      * Get the latest list of DHCP leases that was reported. Must be called on the IpServer looper
      * thread.
diff --git a/Tethering/src/com/android/networkstack/tethering/RequestTracker.java b/Tethering/src/com/android/networkstack/tethering/RequestTracker.java
index 9c61716..3ebe4f7 100644
--- a/Tethering/src/com/android/networkstack/tethering/RequestTracker.java
+++ b/Tethering/src/com/android/networkstack/tethering/RequestTracker.java
@@ -19,8 +19,6 @@
 import static com.android.networkstack.tethering.util.TetheringUtils.createPlaceholderRequest;
 
 import android.net.TetheringManager.TetheringRequest;
-import android.net.ip.IpServer;
-import android.util.ArrayMap;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -30,7 +28,6 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Helper class to keep track of tethering requests.
@@ -39,17 +36,29 @@
  *    layer to start.
  * 2) When the link layer is up, use {@link #getOrCreatePendingRequest(int)} to get a request to
  *    start IP serving with.
- * 3) Remove pending request with {@link #removePendingRequest(TetheringRequest)}.
+ * 3) Remove all pending requests with {@link #removeAllPendingRequests(int)}.
  * Note: This class is not thread-safe.
+ * TODO: Add the pending IIntResultListeners at the same time as the pending requests, and
+ *       call them when we get the tether result.
+ * TODO: Add support for multiple Bluetooth requests before the PAN service connects instead of
+ *       using a separate mPendingPanRequestListeners.
+ * TODO: Add support for fuzzy-matched requests.
  */
 public class RequestTracker {
     private static final String TAG = RequestTracker.class.getSimpleName();
 
-    @NonNull
-    private final boolean mUseFuzzyMatching;
+    private class PendingRequest {
+        @NonNull
+        private final TetheringRequest mTetheringRequest;
 
-    public RequestTracker(boolean useFuzzyMatching) {
-        mUseFuzzyMatching = useFuzzyMatching;
+        private PendingRequest(@NonNull TetheringRequest tetheringRequest) {
+            mTetheringRequest = tetheringRequest;
+        }
+
+        @NonNull
+        TetheringRequest getTetheringRequest() {
+            return mTetheringRequest;
+        }
     }
 
     public enum AddResult {
@@ -60,51 +69,24 @@
         /**
          * Failure indicating that the request could not be added due to a request of the same type
          * with conflicting parameters already pending. If so, we must stop tethering for the
-         * pending request before trying to add the result again. Only returned on V-.
+         * pending request before trying to add the result again.
          */
-        FAILURE_DUPLICATE_REQUEST_RESTART,
-        /**
-         * Failure indicating that the request could not be added due to a fuzzy-matched request
-         * already pending or serving. Only returned on B+.
-         */
-        FAILURE_DUPLICATE_REQUEST_ERROR,
+        FAILURE_CONFLICTING_PENDING_REQUEST
     }
 
     /**
-     * List of pending requests added by {@link #addPendingRequest(TetheringRequest)}
-     * There can be only one per type, since we remove every request of the
-     * same type when we add a request.
+     * List of pending requests added by {@link #addPendingRequest(TetheringRequest)}. There can be
+     * only one per type, since we remove every request of the same type when we add a request.
      */
-    private final List<TetheringRequest> mPendingRequests = new ArrayList<>();
-    /**
-     * List of serving requests added by
-     * {@link #promoteRequestToServing(IpServer, TetheringRequest)}.
-     */
-    private final Map<IpServer, TetheringRequest> mServingRequests = new ArrayMap<>();
+    private final List<PendingRequest> mPendingRequests = new ArrayList<>();
 
     @VisibleForTesting
     List<TetheringRequest> getPendingTetheringRequests() {
-        return new ArrayList<>(mPendingRequests);
-    }
-
-    /**
-     * Adds a pending request or fails with FAILURE_CONFLICTING_REQUEST_FAIL if the request
-     * fuzzy-matches an existing request (either pending or serving).
-     */
-    public AddResult addPendingRequestFuzzyMatched(@NonNull final TetheringRequest newRequest) {
-        List<TetheringRequest> existingRequests = new ArrayList<>();
-        existingRequests.addAll(mServingRequests.values());
-        existingRequests.addAll(mPendingRequests);
-        for (TetheringRequest request : existingRequests) {
-            if (request.fuzzyMatches(newRequest)) {
-                Log.i(TAG, "Cannot add pending request due to existing fuzzy-matched "
-                        + "request: " + request);
-                return AddResult.FAILURE_DUPLICATE_REQUEST_ERROR;
-            }
+        List<TetheringRequest> requests = new ArrayList<>();
+        for (PendingRequest pendingRequest : mPendingRequests) {
+            requests.add(pendingRequest.getTetheringRequest());
         }
-
-        mPendingRequests.add(newRequest);
-        return AddResult.SUCCESS;
+        return requests;
     }
 
     /**
@@ -113,12 +95,9 @@
      * layer comes up. The result of the add operation will be returned as an AddResult code.
      */
     public AddResult addPendingRequest(@NonNull final TetheringRequest newRequest) {
-        if (mUseFuzzyMatching) {
-            return addPendingRequestFuzzyMatched(newRequest);
-        }
-
         // Check the existing requests to see if it is OK to add the new request.
-        for (TetheringRequest existingRequest : mPendingRequests) {
+        for (PendingRequest request : mPendingRequests) {
+            TetheringRequest existingRequest = request.getTetheringRequest();
             if (existingRequest.getTetheringType() != newRequest.getTetheringType()) {
                 continue;
             }
@@ -126,7 +105,7 @@
             // Can't add request if there's a request of the same type with different
             // parameters.
             if (!existingRequest.equalsIgnoreUidPackage(newRequest)) {
-                return AddResult.FAILURE_DUPLICATE_REQUEST_RESTART;
+                return AddResult.FAILURE_CONFLICTING_PENDING_REQUEST;
             }
         }
 
@@ -134,7 +113,7 @@
         // conflicting parameters above, so these would have been equivalent anyway (except for
         // UID).
         removeAllPendingRequests(newRequest.getTetheringType());
-        mPendingRequests.add(newRequest);
+        mPendingRequests.add(new PendingRequest(newRequest));
         return AddResult.SUCCESS;
     }
 
@@ -166,8 +145,10 @@
      */
     @Nullable
     public TetheringRequest getNextPendingRequest(int type) {
-        for (TetheringRequest request : mPendingRequests) {
-            if (request.getTetheringType() == type) return request;
+        for (PendingRequest pendingRequest : mPendingRequests) {
+            TetheringRequest tetheringRequest =
+                    pendingRequest.getTetheringRequest();
+            if (tetheringRequest.getTetheringType() == type) return tetheringRequest;
         }
         return null;
     }
@@ -177,83 +158,7 @@
      *
      * @param type Tethering type
      */
-    public void removeAllPendingRequests(final int type) {
-        mPendingRequests.removeIf(r -> r.getTetheringType() == type);
-    }
-
-    /**
-     * Removes a specific pending request.
-     *
-     * Note: For V-, this will be the same as removeAllPendingRequests to align with historical
-     * behavior.
-     *
-     * @param request Request to be removed
-     */
-    public void removePendingRequest(@NonNull TetheringRequest request) {
-        if (!mUseFuzzyMatching) {
-            // Remove all requests of the same type to match the historical behavior.
-            removeAllPendingRequests(request.getTetheringType());
-            return;
-        }
-
-        mPendingRequests.removeIf(r -> r.equals(request));
-    }
-
-    /**
-     * Removes a tethering request from the pending list and promotes it to serving with the
-     * IpServer that is using it.
-     * Note: If mUseFuzzyMatching is false, then the request will be removed from the pending list,
-     * but it will not be added to serving list.
-     */
-    public void promoteRequestToServing(@NonNull final IpServer ipServer,
-            @NonNull final TetheringRequest tetheringRequest) {
-        removePendingRequest(tetheringRequest);
-        if (!mUseFuzzyMatching) return;
-        mServingRequests.put(ipServer, tetheringRequest);
-    }
-
-
-    /**
-     * Returns the serving request tied to the given IpServer, or null if there is none.
-     * Note: If mUseFuzzyMatching is false, then this will always return null.
-     */
-    @Nullable
-    public TetheringRequest getServingRequest(@NonNull final IpServer ipServer) {
-        return mServingRequests.get(ipServer);
-    }
-
-    /**
-     * Removes the serving request tied to the given IpServer.
-     * Note: If mUseFuzzyMatching is false, then this is a no-op since serving requests are unused
-     * for that configuration.
-     */
-    public void removeServingRequest(@NonNull final IpServer ipServer) {
-        mServingRequests.remove(ipServer);
-    }
-
-    /**
-     * Removes all serving requests of the given tethering type.
-     *
-     * @param type Tethering type
-     */
-    public void removeAllServingRequests(final int type) {
-        mServingRequests.entrySet().removeIf(e -> e.getValue().getTetheringType() == type);
-    }
-
-    /**
-     * Returns an existing (pending or serving) request that fuzzy matches the given request.
-     * Optionally specify matchUid to only return requests with the same uid.
-     */
-    public TetheringRequest findFuzzyMatchedRequest(
-            @NonNull final TetheringRequest tetheringRequest, boolean matchUid) {
-        List<TetheringRequest> allRequests = new ArrayList<>();
-        allRequests.addAll(getPendingTetheringRequests());
-        allRequests.addAll(mServingRequests.values());
-        for (TetheringRequest request : allRequests) {
-            if (!request.fuzzyMatches(tetheringRequest)) continue;
-            if (matchUid && tetheringRequest.getUid() != request.getUid()) continue;
-            return request;
-        }
-        return null;
+    public void removeAllPendingRequests(int type) {
+        mPendingRequests.removeIf(r -> r.getTetheringRequest().getTetheringType() == type);
     }
 }
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 59f05ed..1a26658 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -43,7 +43,6 @@
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 import static android.net.TetheringManager.TETHERING_WIGIG;
 import static android.net.TetheringManager.TETHER_ERROR_BLUETOOTH_SERVICE_PENDING;
-import static android.net.TetheringManager.TETHER_ERROR_DUPLICATE_REQUEST;
 import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
@@ -68,8 +67,6 @@
 import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
 import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
 
-import static com.android.networkstack.tethering.RequestTracker.AddResult.FAILURE_DUPLICATE_REQUEST_ERROR;
-import static com.android.networkstack.tethering.RequestTracker.AddResult.FAILURE_DUPLICATE_REQUEST_RESTART;
 import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_FORCE_USB_FUNCTIONS;
 import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
 import static com.android.networkstack.tethering.UpstreamNetworkMonitor.isCellular;
@@ -312,7 +309,7 @@
         mLooper = mDeps.makeTetheringLooper();
         mNotificationUpdater = mDeps.makeNotificationUpdater(mContext, mLooper);
         mTetheringMetrics = mDeps.makeTetheringMetrics(mContext);
-        mRequestTracker = new RequestTracker(isTetheringWithSoftApConfigEnabled());
+        mRequestTracker = new RequestTracker();
 
         mTetherStates = new ArrayMap<>();
         mConnectedClientsTracker = new ConnectedClientsTracker();
@@ -463,7 +460,6 @@
         return mSettingsObserver;
     }
 
-    // TODO: Replace with SdkLevel.isAtLeastB() once the feature is fully implemented.
     boolean isTetheringWithSoftApConfigEnabled() {
         return SdkLevel.isAtLeastB() && Flags.tetheringWithSoftApConfig();
     }
@@ -708,16 +704,9 @@
             RequestTracker.AddResult result = mRequestTracker.addPendingRequest(request);
             // If tethering is already pending with a conflicting request, stop tethering before
             // starting.
-            if (result == FAILURE_DUPLICATE_REQUEST_RESTART) {
+            if (result == RequestTracker.AddResult.FAILURE_CONFLICTING_PENDING_REQUEST) {
                 stopTetheringInternal(type); // Also removes the request from the tracker.
                 mRequestTracker.addPendingRequest(request);
-            } else if (result == FAILURE_DUPLICATE_REQUEST_ERROR) {
-                // Reject any fuzzy matched request.
-                // TODO: Add a CTS test to verify back-to-back start/stop calls succeed. This must
-                // be for a non-Wifi type, since Wifi will reject the start calls if it hasn't
-                // brought down the SoftAP yet.
-                sendTetherResult(listener, TETHER_ERROR_DUPLICATE_REQUEST);
-                return;
             }
 
             if (request.isExemptFromEntitlementCheck()) {
@@ -737,28 +726,39 @@
         });
     }
 
+    private boolean isTetheringTypePendingOrServing(final int type) {
+        if (mRequestTracker.getNextPendingRequest(type) != null) return true;
+        for (TetherState state : mTetherStates.values()) {
+            // TODO: isCurrentlyServing only starts returning true once the IpServer has processed
+            // the CMD_TETHER_REQUESTED. Ensure that we consider the request to be serving even when
+            // that has not happened yet.
+            if (state.isCurrentlyServing() && state.ipServer.interfaceType() == type) return true;
+        }
+        return false;
+    }
+
     void stopTetheringRequest(@NonNull final TetheringRequest request,
             @NonNull final IIntResultListener listener) {
         if (!isTetheringWithSoftApConfigEnabled()) return;
-        final boolean hasNetworkSettings = hasCallingPermission(NETWORK_SETTINGS);
         mHandler.post(() -> {
-            if (mRequestTracker.findFuzzyMatchedRequest(request, !hasNetworkSettings) != null) {
-                final int type = request.getTetheringType();
+            final int type = request.getTetheringType();
+            if (isTetheringTypePendingOrServing(type)) {
                 stopTetheringInternal(type);
-                // TODO: We should send the success result after the waiting for tethering to
-                //       actually stop.
-                sendTetherResult(listener, TETHER_ERROR_NO_ERROR);
+                try {
+                    listener.onResult(TETHER_ERROR_NO_ERROR);
+                } catch (RemoteException ignored) { }
                 return;
             }
 
             // Request doesn't match any active requests, ignore.
-            sendTetherResult(listener, TETHER_ERROR_UNKNOWN_REQUEST);
+            try {
+                listener.onResult(TETHER_ERROR_UNKNOWN_REQUEST);
+            } catch (RemoteException ignored) { }
         });
     }
 
     void stopTetheringInternal(int type) {
         mRequestTracker.removeAllPendingRequests(type);
-        mRequestTracker.removeAllServingRequests(type);
 
         // Using a placeholder here is ok since none of the disable APIs use the request for
         // anything. We simply need the tethering type to know which link layer to poke for removal.
@@ -802,25 +802,21 @@
         // The result of Bluetooth tethering will be sent after the pan service connects.
         if (result == TETHER_ERROR_BLUETOOTH_SERVICE_PENDING) return;
 
-        sendTetherResultAndRemoveOnError(request, listener, result);
+        sendTetherResult(listener, result, type);
     }
 
-    private void sendTetherResult(final IIntResultListener listener, final int result) {
+    private void sendTetherResult(final IIntResultListener listener, final int result,
+            final int type) {
         if (listener != null) {
             try {
                 listener.onResult(result);
-            } catch (RemoteException e) {
-            }
+            } catch (RemoteException e) { }
         }
-    }
 
-    private void sendTetherResultAndRemoveOnError(TetheringRequest request,
-            final IIntResultListener listener, final int result) {
-        sendTetherResult(listener, result);
-
+        // If changing tethering fail, remove corresponding request
+        // no matter who trigger the start/stop.
         if (result != TETHER_ERROR_NO_ERROR) {
-            mRequestTracker.removePendingRequest(request);
-            final int type = request.getTetheringType();
+            mRequestTracker.removeAllPendingRequests(type);
             mTetheringMetrics.updateErrorCode(type, result);
             mTetheringMetrics.sendReport(type);
         }
@@ -866,7 +862,8 @@
             // the service just to stop tethering since tethering is not started. Just remove
             // any pending request to enable tethering, and notify them that they have failed.
             if (mPendingPanRequestListener != null) {
-                sendTetherResult(mPendingPanRequestListener, TETHER_ERROR_SERVICE_UNAVAIL);
+                sendTetherResult(mPendingPanRequestListener, TETHER_ERROR_SERVICE_UNAVAIL,
+                        TETHERING_BLUETOOTH);
             }
             mPendingPanRequestListener = null;
             return TETHER_ERROR_NO_ERROR;
@@ -907,10 +904,7 @@
                 if (mPendingPanRequestListener != null) {
                     final int result = setBluetoothTetheringSettings(mBluetoothPan,
                             true /* enable */);
-                    sendTetherResultAndRemoveOnError(
-                            mRequestTracker.getOrCreatePendingRequest(TETHERING_BLUETOOTH),
-                            mPendingPanRequestListener,
-                            result);
+                    sendTetherResult(mPendingPanRequestListener, result, TETHERING_BLUETOOTH);
                 }
                 mPendingPanRequestListener = null;
             });
@@ -924,10 +918,8 @@
                 mIsConnected = false;
 
                 if (mPendingPanRequestListener != null) {
-                    sendTetherResultAndRemoveOnError(
-                            mRequestTracker.getOrCreatePendingRequest(TETHERING_BLUETOOTH),
-                            mPendingPanRequestListener,
-                            TETHER_ERROR_SERVICE_UNAVAIL);
+                    sendTetherResult(mPendingPanRequestListener, TETHER_ERROR_SERVICE_UNAVAIL,
+                            TETHERING_BLUETOOTH);
                 }
                 mPendingPanRequestListener = null;
                 mBluetoothIfaceRequest = null;
@@ -1101,7 +1093,9 @@
         final int type = ifaceNameToType(iface);
         if (type == TETHERING_INVALID) {
             Log.e(TAG, "Ignoring call to legacy tether for unknown iface " + iface);
-            sendTetherResult(listener, TETHER_ERROR_UNKNOWN_IFACE);
+            try {
+                listener.onResult(TETHER_ERROR_UNKNOWN_IFACE);
+            } catch (RemoteException e) { }
         }
 
         TetheringRequest request = mRequestTracker.getNextPendingRequest(type);
@@ -1138,7 +1132,9 @@
                 // Do nothing
                 break;
         }
-        sendTetherResult(listener, result);
+        try {
+            listener.onResult(result);
+        } catch (RemoteException e) { }
     }
 
     /**
@@ -1171,9 +1167,11 @@
             Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
             return TETHER_ERROR_UNAVAIL_IFACE;
         }
-        mRequestTracker.promoteRequestToServing(tetherState.ipServer, request);
         // NOTE: If a CMD_TETHER_REQUESTED message is already in the IpServer's queue but not yet
         // processed, this will be a no-op and it will not return an error.
+        //
+        // This code cannot race with untether() because they both run on the handler thread.
+        mRequestTracker.removeAllPendingRequests(request.getTetheringType());
         tetherState.ipServer.enable(request);
         if (request.getRequestType() == REQUEST_TYPE_PLACEHOLDER) {
             TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
@@ -1193,7 +1191,10 @@
             return;
         }
         mHandler.post(() -> {
-            sendTetherResult(listener, legacyUntetherInternal(iface));
+            try {
+                listener.onResult(legacyUntetherInternal(iface));
+            } catch (RemoteException e) {
+            }
         });
     }
 
@@ -1208,7 +1209,7 @@
             Log.e(TAG, "Tried to untether an inactive iface :" + iface + ", ignoring");
             return TETHER_ERROR_UNAVAIL_IFACE;
         }
-        ensureIpServerUnwanted(tetherState.ipServer);
+        tetherState.ipServer.unwanted();
         return TETHER_ERROR_NO_ERROR;
     }
 
@@ -1288,12 +1289,7 @@
             final TetherState tetherState = mTetherStates.valueAt(i);
             final int type = tetherState.ipServer.interfaceType();
             final String iface = mTetherStates.keyAt(i);
-            // Note: serving requests are only populated on B+. B+ also uses the sync state
-            // machine by default. This ensures that the serving request is (correctly) populated
-            // after the IpServer enters the available state and before it enters the serving
-            // state.
-            final TetheringRequest request =
-                    mRequestTracker.getServingRequest(tetherState.ipServer);
+            final TetheringRequest request = tetherState.ipServer.getTetheringRequest();
             final boolean includeSoftApConfig = request != null && cookie != null
                     && (cookie.uid == request.getUid() || cookie.hasSystemPrivilege);
             final TetheringInterface tetheringIface = new TetheringInterface(type, iface,
@@ -1615,7 +1611,7 @@
 
     private void disableWifiIpServingCommon(int tetheringType, String ifname) {
         if (!TextUtils.isEmpty(ifname) && mTetherStates.containsKey(ifname)) {
-            ensureIpServerUnwanted(mTetherStates.get(ifname).ipServer);
+            mTetherStates.get(ifname).ipServer.unwanted();
             return;
         }
 
@@ -1632,7 +1628,7 @@
         for (int i = 0; i < mTetherStates.size(); i++) {
             final IpServer ipServer = mTetherStates.valueAt(i).ipServer;
             if (ipServer.interfaceType() == tetheringType) {
-                ensureIpServerUnwanted(ipServer);
+                ipServer.unwanted();
                 return;
             }
         }
@@ -1779,7 +1775,9 @@
 
     void setUsbTethering(boolean enable, IIntResultListener listener) {
         mHandler.post(() -> {
-            sendTetherResult(listener, setUsbTethering(enable));
+            try {
+                listener.onResult(setUsbTethering(enable));
+            } catch (RemoteException e) { }
         });
     }
 
@@ -2906,9 +2904,6 @@
                 tetherState.lastState = state;
                 tetherState.lastError = lastError;
             } else {
-                // Note: Even if an IpServer exists for this iface, it may be different from "who"
-                // if a new IpServer fills the gap before the IpServer.STATE_UNAVAILABLE transition.
-                // TODO: remove this comment once the sync state machine is enabled everywhere.
                 if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
             }
 
@@ -2923,19 +2918,7 @@
             int which;
             switch (state) {
                 case IpServer.STATE_UNAVAILABLE:
-                    which = TetherMainSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
-                    break;
                 case IpServer.STATE_AVAILABLE:
-                    if (lastError != TETHER_ERROR_NO_ERROR) {
-                        // IpServer transitioned from an enabled state (STATE_TETHERED or
-                        // STATE_LOCAL_ONLY) back to STATE_AVAILABLE due to an error, so make sure
-                        // we remove the serving request from RequestTracker.
-                        // TODO: don't continue to use IpServers after they have hit an error, and
-                        // instead move them to STATE_UNAVAILABLE. This code can then
-                        // unconditionally remove the serving request whenever the IpServer enters
-                        // STATE_UNAVAILABLE.
-                        mRequestTracker.removeServingRequest(who);
-                    }
                     which = TetherMainSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
                     break;
                 case IpServer.STATE_TETHERED:
@@ -3031,18 +3014,11 @@
         final TetherState tetherState = mTetherStates.get(iface);
         if (tetherState == null) return;
 
-        mRequestTracker.removeServingRequest(tetherState.ipServer);
         tetherState.ipServer.stop();
         mLog.i("removing IpServer for: " + iface);
         mTetherStates.remove(iface);
     }
 
-    private void ensureIpServerUnwanted(final IpServer ipServer) {
-        mLog.i("unrequesting IpServer: " + ipServer);
-        mRequestTracker.removeServingRequest(ipServer);
-        ipServer.unwanted();
-    }
-
     private static String[] copy(String[] strarray) {
         return Arrays.copyOf(strarray, strarray.length);
     }
@@ -3050,7 +3026,9 @@
     void setPreferTestNetworks(final boolean prefer, IIntResultListener listener) {
         mHandler.post(() -> {
             mUpstreamNetworkMonitor.setPreferTestNetworks(prefer);
-            sendTetherResult(listener, TETHER_ERROR_NO_ERROR);
+            try {
+                listener.onResult(TETHER_ERROR_NO_ERROR);
+            } catch (RemoteException e) { }
         });
     }
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/RequestTrackerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/RequestTrackerTest.java
index 086f2d2..e00e9f0 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/RequestTrackerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/RequestTrackerTest.java
@@ -18,22 +18,19 @@
 
 import static android.net.TetheringManager.TETHERING_USB;
 import static android.net.TetheringManager.TETHERING_WIFI;
-import static android.net.TetheringManager.TETHERING_VIRTUAL;
 
 import static com.android.networkstack.tethering.util.TetheringUtils.createPlaceholderRequest;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.Mockito.mock;
-
 import android.net.TetheringManager.TetheringRequest;
-import android.net.ip.IpServer;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.networkstack.tethering.RequestTracker.AddResult;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -42,10 +39,13 @@
 public class RequestTrackerTest {
     private RequestTracker mRequestTracker;
 
+    @Before
+    public void setUp() {
+        mRequestTracker = new RequestTracker();
+    }
+
     @Test
     public void testNoRequestsAdded_noPendingRequests() {
-        mRequestTracker = new RequestTracker(false);
-
         assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isNull();
         assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI))
                 .isEqualTo(createPlaceholderRequest(TETHERING_WIFI));
@@ -53,7 +53,6 @@
 
     @Test
     public void testAddRequest_successResultAndBecomesNextPending() {
-        mRequestTracker = new RequestTracker(false);
         final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
 
         final AddResult result = mRequestTracker.addPendingRequest(request);
@@ -65,7 +64,6 @@
 
     @Test
     public void testAddRequest_equalRequestExists_successResultAndBecomesNextPending() {
-        mRequestTracker = new RequestTracker(false);
         final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
         mRequestTracker.addPendingRequest(request);
 
@@ -79,7 +77,6 @@
 
     @Test
     public void testAddRequest_equalButDifferentUidRequest_successResultAndBecomesNextPending() {
-        mRequestTracker = new RequestTracker(false);
         final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
         request.setUid(1000);
         request.setPackageName("package");
@@ -97,8 +94,7 @@
     }
 
     @Test
-    public void testAddRequest_conflictingPendingRequest_returnsFailureConflictingRequestRestart() {
-        mRequestTracker = new RequestTracker(false);
+    public void testAddConflictingRequest_returnsFailureConflictingPendingRequest() {
         final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
         final TetheringRequest conflictingRequest = new TetheringRequest.Builder(TETHERING_WIFI)
                 .setExemptFromEntitlementCheck(true).build();
@@ -106,139 +102,13 @@
 
         final AddResult result = mRequestTracker.addPendingRequest(conflictingRequest);
 
-        assertThat(result).isEqualTo(AddResult.FAILURE_DUPLICATE_REQUEST_RESTART);
+        assertThat(result).isEqualTo(AddResult.FAILURE_CONFLICTING_PENDING_REQUEST);
         assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isEqualTo(request);
         assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI)).isEqualTo(request);
     }
 
     @Test
-    public void testAddRequest_noExistingRequestsFuzzyMatching_returnsSuccess() {
-        mRequestTracker = new RequestTracker(true);
-        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
-
-        final AddResult result = mRequestTracker.addPendingRequest(request);
-
-        assertThat(result).isEqualTo(AddResult.SUCCESS);
-        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isEqualTo(request);
-        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI)).isEqualTo(request);
-    }
-
-    @Test
-    public void testAddRequest_conflictingPendingRequestFuzzyMatching_returnsFailure() {
-        mRequestTracker = new RequestTracker(true);
-        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
-        final TetheringRequest conflictingRequest = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setExemptFromEntitlementCheck(true).build();
-        mRequestTracker.addPendingRequest(request);
-
-        final AddResult result = mRequestTracker.addPendingRequest(conflictingRequest);
-
-        assertThat(result).isEqualTo(AddResult.FAILURE_DUPLICATE_REQUEST_ERROR);
-        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isEqualTo(request);
-        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI)).isEqualTo(request);
-    }
-
-    @Test
-    public void testAddRequest_conflictingServingRequestFuzzyMatching_returnsFailure() {
-        mRequestTracker = new RequestTracker(true);
-        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
-        final TetheringRequest conflictingRequest = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setExemptFromEntitlementCheck(true).build();
-        mRequestTracker.promoteRequestToServing(mock(IpServer.class), request);
-
-        final AddResult result = mRequestTracker.addPendingRequest(conflictingRequest);
-
-        assertThat(result).isEqualTo(AddResult.FAILURE_DUPLICATE_REQUEST_ERROR);
-        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isNull();
-        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI))
-                .isEqualTo(createPlaceholderRequest(TETHERING_WIFI));
-    }
-
-    @Test
-    public void testAddRequest_nonMatchingPendingRequestFuzzyMatching_returnsSuccess() {
-        mRequestTracker = new RequestTracker(true);
-        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_VIRTUAL).build();
-        final TetheringRequest nonFuzzyMatched = new TetheringRequest.Builder(TETHERING_VIRTUAL)
-                .setInterfaceName("iface")
-                .build();
-        mRequestTracker.addPendingRequest(request);
-
-        final AddResult result = mRequestTracker.addPendingRequest(nonFuzzyMatched);
-
-        assertThat(result).isEqualTo(AddResult.SUCCESS);
-        // Next request is still the first, but verify RequestTracker contains the second request by
-        // seeing if it rejects anything matching the second request
-        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_VIRTUAL)).isEqualTo(request);
-        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_VIRTUAL))
-                .isEqualTo(request);
-        assertThat(mRequestTracker.addPendingRequestFuzzyMatched(nonFuzzyMatched))
-                .isEqualTo(AddResult.FAILURE_DUPLICATE_REQUEST_ERROR);
-    }
-
-    @Test
-    public void testAddRequest_nonMatchingServingRequestFuzzyMatching_returnsSuccess() {
-        mRequestTracker = new RequestTracker(true);
-        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_VIRTUAL).build();
-        final TetheringRequest nonFuzzyMatched = new TetheringRequest.Builder(TETHERING_VIRTUAL)
-                .setInterfaceName("iface")
-                .build();
-        mRequestTracker.promoteRequestToServing(mock(IpServer.class), request);
-
-        final AddResult result = mRequestTracker.addPendingRequest(nonFuzzyMatched);
-
-        assertThat(result).isEqualTo(AddResult.SUCCESS);
-        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_VIRTUAL))
-                .isEqualTo(nonFuzzyMatched);
-        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_VIRTUAL))
-                .isEqualTo(nonFuzzyMatched);
-    }
-
-    @Test
-    public void testRemovePendingRequest_removesAllPendingRequestsOfType() {
-        mRequestTracker = new RequestTracker(false);
-        final TetheringRequest request1 = new TetheringRequest.Builder(TETHERING_WIFI).build();
-        request1.setUid(1000);
-        request1.setPackageName("package");
-        mRequestTracker.addPendingRequest(request1);
-        final TetheringRequest request2 = new TetheringRequest.Builder(TETHERING_WIFI).build();
-        request2.setUid(2000);
-        request2.setPackageName("package2");
-
-        mRequestTracker.removePendingRequest(request2);
-
-        // Verify request1 isn't pending even though we tried to remove a different request
-        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isNull();
-        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI))
-                .isEqualTo(createPlaceholderRequest(TETHERING_WIFI));
-    }
-
-    @Test
-    public void testRemovePendingRequest_fuzzyMatching_onlyTheEqualRequestIsRemoved() {
-        mRequestTracker = new RequestTracker(true);
-        final TetheringRequest request1 = new TetheringRequest.Builder(TETHERING_VIRTUAL).build();
-        final TetheringRequest request2 = new TetheringRequest.Builder(TETHERING_VIRTUAL)
-                .setInterfaceName("iface")
-                .build();
-        mRequestTracker.addPendingRequest(request1);
-        mRequestTracker.addPendingRequest(request2);
-
-        mRequestTracker.removePendingRequest(request2);
-
-        // Verify request1 is still pending.
-        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_VIRTUAL)).isEqualTo(request1);
-        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_VIRTUAL))
-                .isEqualTo(request1);
-        assertThat(mRequestTracker.addPendingRequestFuzzyMatched(request1))
-                .isEqualTo(AddResult.FAILURE_DUPLICATE_REQUEST_ERROR);
-        // Verify we've removed request2 by checking if it can be added back without
-        // FAILURE_CONFLICTING_REQUEST_FAIL.
-        assertThat(mRequestTracker.addPendingRequestFuzzyMatched(request2))
-                .isEqualTo(AddResult.SUCCESS);
-    }
-
-    @Test
     public void testRemoveAllPendingRequests_noPendingRequestsLeft() {
-        mRequestTracker = new RequestTracker(false);
         final TetheringRequest firstRequest = new TetheringRequest.Builder(TETHERING_WIFI).build();
         firstRequest.setUid(1000);
         firstRequest.setPackageName("package");
@@ -257,7 +127,6 @@
 
     @Test
     public void testRemoveAllPendingRequests_differentTypeExists_doesNotRemoveDifferentType() {
-        mRequestTracker = new RequestTracker(false);
         final TetheringRequest differentType = new TetheringRequest.Builder(TETHERING_USB).build();
         mRequestTracker.addPendingRequest(differentType);
 
@@ -267,61 +136,4 @@
         assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_USB))
                 .isEqualTo(differentType);
     }
-
-    @Test
-    public void testPromoteRequestToServing_requestIsntPendingAnymore() {
-        mRequestTracker = new RequestTracker(false);
-        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
-        mRequestTracker.addPendingRequest(request);
-
-        mRequestTracker.promoteRequestToServing(mock(IpServer.class), request);
-
-        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isNull();
-        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI))
-                .isEqualTo(createPlaceholderRequest(TETHERING_WIFI));
-    }
-
-    @Test
-    public void testPromoteRequestToServing_fuzzyMatching_requestIsntPendingAnymore() {
-        mRequestTracker = new RequestTracker(true);
-        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
-        mRequestTracker.addPendingRequest(request);
-
-        mRequestTracker.promoteRequestToServing(mock(IpServer.class), request);
-
-        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isNull();
-        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI))
-                .isEqualTo(createPlaceholderRequest(TETHERING_WIFI));
-    }
-
-    @Test
-    public void testRemoveServingRequest_fuzzyMatching_requestCanBeAddedAgain() {
-        mRequestTracker = new RequestTracker(true);
-        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
-        mRequestTracker.addPendingRequest(request);
-        IpServer ipServer = mock(IpServer.class);
-        mRequestTracker.promoteRequestToServing(ipServer, request);
-
-        mRequestTracker.removeServingRequest(ipServer);
-
-        AddResult result = mRequestTracker.addPendingRequest(request);
-        assertThat(result).isEqualTo(AddResult.SUCCESS);
-        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isEqualTo(request);
-        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI)).isEqualTo(request);
-    }
-
-    @Test
-    public void testRemoveAllServingRequests_fuzzyMatching_requestCanBeAddedAgain() {
-        mRequestTracker = new RequestTracker(true);
-        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
-        mRequestTracker.addPendingRequest(request);
-        mRequestTracker.promoteRequestToServing(mock(IpServer.class), request);
-
-        mRequestTracker.removeAllServingRequests(TETHERING_WIFI);
-
-        AddResult result = mRequestTracker.addPendingRequest(request);
-        assertThat(result).isEqualTo(AddResult.SUCCESS);
-        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isEqualTo(request);
-        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI)).isEqualTo(request);
-    }
 }
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 67df98c..12a04b0 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -50,18 +50,15 @@
 import static android.net.TetheringManager.TETHERING_VIRTUAL;
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
-import static android.net.TetheringManager.TETHER_ERROR_DUPLICATE_REQUEST;
 import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
 import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
 import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
-import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_REQUEST;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED;
 import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED;
 import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
-import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
 import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
@@ -96,7 +93,6 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.notNull;
@@ -201,7 +197,6 @@
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.modules.utils.build.SdkLevel;
-import com.android.net.flags.Flags;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.PrivateAddressCoordinator;
@@ -313,7 +308,8 @@
     @Mock private TetheringMetrics mTetheringMetrics;
     @Mock private PrivateAddressCoordinator.Dependencies mPrivateAddressCoordinatorDependencies;
 
-    private MockIpServerDependencies mIpServerDependencies;
+    private final MockIpServerDependencies mIpServerDependencies =
+            spy(new MockIpServerDependencies());
     private final MockTetheringDependencies mTetheringDependencies =
             new MockTetheringDependencies();
 
@@ -400,9 +396,6 @@
     }
 
     public class MockIpServerDependencies extends IpServer.Dependencies {
-
-        private int mOnDhcpServerCreatedResult = STATUS_SUCCESS;
-
         @Override
         public DadProxy getDadProxy(
                 Handler handler, InterfaceParams ifParams) {
@@ -444,7 +437,7 @@
                 DhcpServerCallbacks cb) {
             new Thread(() -> {
                 try {
-                    cb.onDhcpServerCreated(mOnDhcpServerCreatedResult, mDhcpServer);
+                    cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer);
                 } catch (RemoteException e) {
                     fail(e.getMessage());
                 }
@@ -455,10 +448,6 @@
                 IpNeighborMonitor.NeighborEventConsumer c) {
             return mIpNeighborMonitor;
         }
-
-        public void setOnDhcpServerCreatedResult(final int result) {
-            mOnDhcpServerCreatedResult = result;
-        }
     }
 
     public class MockTetheringDependencies extends TetheringDependencies {
@@ -719,7 +708,6 @@
 
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
-        mIpServerDependencies = spy(new MockIpServerDependencies());
     }
 
     // In order to interact with syncSM from the test, tethering must be created in test thread.
@@ -2437,7 +2425,7 @@
 
     @Test
     public void testSoftApConfigInTetheringEventCallback() throws Exception {
-        assumeTrue(isTetheringWithSoftApConfigEnabled());
+        assumeTrue(SdkLevel.isAtLeastV());
         when(mContext.checkCallingOrSelfPermission(NETWORK_SETTINGS))
                 .thenReturn(PERMISSION_DENIED);
         when(mContext.checkCallingOrSelfPermission(NETWORK_STACK))
@@ -2494,12 +2482,14 @@
         // Enable wifi tethering
         mBinderCallingUid = TEST_CALLER_UID;
         mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, null);
-        mLooper.dispatchAll();
         mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
         mLooper.dispatchAll();
-        // Netd "up" event should not trigger a state change callback in B+.
-        callback.assertNoStateChangeCallback();
+        if (SdkLevel.isAtLeastB()) {
+            // Starting in B, ignore the interfaceStatusChanged
+            callback.assertNoStateChangeCallback();
+        }
         sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+        mLooper.dispatchAll();
         // Verify we see  Available -> Tethered states
         assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
                 callback.pollTetherStatesChanged().availableList);
@@ -2517,15 +2507,20 @@
         callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STARTED);
 
         // Disable wifi tethering
-        mTethering.stopTethering(TETHERING_WIFI);
         mLooper.dispatchAll();
-        sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME,
+        mTethering.stopTethering(TETHERING_WIFI);
+        sendWifiApStateChanged(WIFI_AP_STATE_DISABLED);
+        if (isAtLeastT()) {
+            // After T, tethering doesn't support WIFI_AP_STATE_DISABLED with null interface name.
+            callback.assertNoStateChangeCallback();
+            sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME,
                     IFACE_IP_MODE_TETHERED);
-        assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
+        }
+        assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
                 callback.pollTetherStatesChanged().availableList);
         assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
                 differentCallback.pollTetherStatesChanged().availableList);
-        assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
+        assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
                 settingsCallback.pollTetherStatesChanged().availableList);
         mLooper.dispatchAll();
         callback.expectUpstreamChanged(NULL_NETWORK);
@@ -2533,332 +2528,6 @@
         callback.assertNoCallback();
     }
 
-    private boolean isTetheringWithSoftApConfigEnabled() {
-        return SdkLevel.isAtLeastB() && Flags.tetheringWithSoftApConfig();
-    }
-
-    @Test
-    public void testFuzzyMatchedWifiCannotBeAdded() throws Exception {
-        assumeTrue(isTetheringWithSoftApConfigEnabled());
-        initTetheringOnTestThread();
-        TestTetheringEventCallback callback = new TestTetheringEventCallback();
-        SoftApConfiguration softApConfig = new SoftApConfiguration.Builder().setWifiSsid(
-                WifiSsid.fromBytes("SoftApConfig".getBytes(StandardCharsets.UTF_8))).build();
-        final TetheringInterface wifiIfaceWithoutConfig = new TetheringInterface(
-                TETHERING_WIFI, TEST_WLAN_IFNAME, null);
-        final TetheringInterface wifiIfaceWithConfig = new TetheringInterface(
-                TETHERING_WIFI, TEST_WLAN_IFNAME, softApConfig);
-
-        // Register callback before running any tethering.
-        mTethering.registerTetheringEventCallback(callback);
-        mLooper.dispatchAll();
-        callback.expectTetheredClientChanged(Collections.emptyList());
-        callback.expectUpstreamChanged(NULL_NETWORK);
-        callback.expectConfigurationChanged(
-                mTethering.getTetheringConfiguration().toStableParcelable());
-        assertTetherStatesNotNullButEmpty(callback.pollTetherStatesChanged());
-        callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
-        UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
-        initTetheringUpstream(upstreamState);
-
-        // Start wifi tethering but don't trigger the link layer event yet.
-        mBinderCallingUid = TEST_CALLER_UID;
-        final TetheringRequest tetheringRequest = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig).build();
-        tetheringRequest.setUid(TEST_CALLER_UID);
-        ResultListener successListener = new ResultListener(TETHER_ERROR_NO_ERROR);
-        when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener);
-        mLooper.dispatchAll();
-        successListener.assertHasResult();
-
-        // Try starting wifi tethering with various fuzzy-matching requests and verify we get
-        // TETHER_ERROR_DUPLICATE_REQUEST.
-
-        // Different static IP addresses
-        final TetheringRequest differentIpAddr = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig)
-                .setStaticIpv4Addresses(new LinkAddress("192.168.0.123/24"),
-                        new LinkAddress("192.168.0.42/24"))
-                .build();
-        differentIpAddr.setUid(TEST_CALLER_UID);
-        ResultListener differentIpAddrListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
-        mTethering.startTethering(differentIpAddr, TEST_CALLER_PKG, differentIpAddrListener);
-        mLooper.dispatchAll();
-        verify(mWifiManager, times(1)).startTetheredHotspot(any());
-        verify(mWifiManager, never()).stopSoftAp();
-        differentIpAddrListener.assertHasResult();
-
-        // Different UID
-        final TetheringRequest differentUid = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig).build();
-        differentUid.setUid(TEST_CALLER_UID + 1);
-        ResultListener differentUidListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
-        mTethering.startTethering(differentUid, TEST_CALLER_PKG, differentUidListener);
-        mLooper.dispatchAll();
-        differentUidListener.assertHasResult();
-        verify(mWifiManager, times(1)).startTetheredHotspot(any());
-        verify(mWifiManager, never()).stopSoftAp();
-
-        // Mock the link layer event to start IP serving and verify we still get
-        // TETHER_ERROR_DUPLICATE_REQUEST even though the request is no longer pending and is
-        // already serving.
-        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
-        assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
-                callback.pollTetherStatesChanged().availableList);
-        assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
-                callback.pollTetherStatesChanged().tetheredList);
-        callback.expectUpstreamChanged(upstreamState.network);
-        callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STARTED);
-        differentIpAddrListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
-        differentUidListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
-        mTethering.startTethering(differentIpAddr, TEST_CALLER_PKG, differentIpAddrListener);
-        mTethering.startTethering(differentUid, TEST_CALLER_PKG, differentUidListener);
-        mLooper.dispatchAll();
-        differentIpAddrListener.assertHasResult();
-        differentUidListener.assertHasResult();
-        verify(mWifiManager, times(1)).startTetheredHotspot(any());
-        verify(mWifiManager, never()).stopSoftAp();
-    }
-
-    @Test
-    public void testFuzzyMatchedWifiCanBeAddedAfterIpServerStopped() throws Exception {
-        assumeTrue(isTetheringWithSoftApConfigEnabled());
-        when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
-        initTetheringOnTestThread();
-
-        // Start wifi tethering and mock the ap state change.
-        SoftApConfiguration softApConfig = new SoftApConfiguration.Builder().setWifiSsid(
-                WifiSsid.fromBytes("SoftApConfig".getBytes(StandardCharsets.UTF_8))).build();
-        final TetheringRequest tetheringRequest = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig).build();
-        ResultListener successListener = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener);
-        mLooper.dispatchAll();
-        successListener.assertHasResult();
-        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
-
-        // Starting wifi again will cause TETHER_ERROR_DUPLICATE_REQUEST
-        ResultListener failureListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, failureListener);
-        mLooper.dispatchAll();
-        failureListener.assertHasResult();
-
-        // Trigger Netd callback to stop the IpServer
-        mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, false);
-
-        // We should be able to request the same Wifi again
-        ResultListener successListener2 = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener2);
-        mLooper.dispatchAll();
-        successListener2.assertHasResult();
-    }
-
-    @Test
-    public void testFuzzyMatchedWifiCanBeAddedAfterIpServerUnwanted() throws Exception {
-        assumeTrue(isTetheringWithSoftApConfigEnabled());
-        when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
-        initTetheringOnTestThread();
-
-        // Start wifi tethering and mock the ap state change.
-        SoftApConfiguration softApConfig = new SoftApConfiguration.Builder().setWifiSsid(
-                WifiSsid.fromBytes("SoftApConfig".getBytes(StandardCharsets.UTF_8))).build();
-        final TetheringRequest tetheringRequest = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig).build();
-        ResultListener successListener = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener);
-        mLooper.dispatchAll();
-        successListener.assertHasResult();
-        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
-
-        // Starting wifi again will cause TETHER_ERROR_DUPLICATE_REQUEST
-        ResultListener failureListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, failureListener);
-        mLooper.dispatchAll();
-        failureListener.assertHasResult();
-
-        // Trigger wifi ap state change to tell IpServer it's unwanted.
-        sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
-
-        // We should be able to request the same Wifi again
-        ResultListener successListener2 = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener2);
-        mLooper.dispatchAll();
-        successListener2.assertHasResult();
-    }
-
-    @Test
-    public void testFuzzyMatchedWifiCanBeAddedAfterIpServerError() throws Exception {
-        assumeTrue(isTetheringWithSoftApConfigEnabled());
-        when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
-        initTetheringOnTestThread();
-
-        // Set up the DHCP server to fail creation.
-        mIpServerDependencies.setOnDhcpServerCreatedResult(STATUS_UNKNOWN_ERROR);
-
-        // Start wifi tethering and mock the ap state change.
-        SoftApConfiguration softApConfig = new SoftApConfiguration.Builder().setWifiSsid(
-                WifiSsid.fromBytes("SoftApConfig".getBytes(StandardCharsets.UTF_8))).build();
-        final TetheringRequest tetheringRequest = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig).build();
-        ResultListener successListener = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener);
-        mLooper.dispatchAll();
-        successListener.assertHasResult();
-        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
-
-        // We should be able to request the same Wifi again since the DHCP server transitioned the
-        // IpServer back to InitialState.
-        ResultListener successListener2 = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener2);
-        mLooper.dispatchAll();
-        successListener2.assertHasResult();
-    }
-
-    @Test
-    public void testFuzzyMatchedWifiCanBeAddedAfterStoppingPendingRequest() throws Exception {
-        assumeTrue(isTetheringWithSoftApConfigEnabled());
-        when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
-        initTetheringOnTestThread();
-
-        // Start wifi tethering but keep the request pending by not sending the ap state change.
-        SoftApConfiguration softApConfig = new SoftApConfiguration.Builder().setWifiSsid(
-                WifiSsid.fromBytes("SoftApConfig".getBytes(StandardCharsets.UTF_8))).build();
-        final TetheringRequest tetheringRequest = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig).build();
-        ResultListener successListener = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener);
-        mLooper.dispatchAll();
-        successListener.assertHasResult();
-
-        // Starting wifi again will cause TETHER_ERROR_DUPLICATE_REQUEST
-        ResultListener failureListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, failureListener);
-        mLooper.dispatchAll();
-        failureListener.assertHasResult();
-
-        // Stop Wifi tethering.
-        mTethering.stopTethering(TETHERING_WIFI);
-
-        // We should be able to request the same Wifi again
-        ResultListener successListener2 = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener2);
-        mLooper.dispatchAll();
-        successListener2.assertHasResult();
-    }
-
-    @Test
-    public void testFuzzyMatchedWifiCanBeAddedAfterStoppingServingRequest() throws Exception {
-        assumeTrue(isTetheringWithSoftApConfigEnabled());
-        when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
-        initTetheringOnTestThread();
-
-        // Start wifi tethering and mock the ap state change.
-        SoftApConfiguration softApConfig = new SoftApConfiguration.Builder().setWifiSsid(
-                WifiSsid.fromBytes("SoftApConfig".getBytes(StandardCharsets.UTF_8))).build();
-        final TetheringRequest tetheringRequest = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig).build();
-        ResultListener successListener = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener);
-        mLooper.dispatchAll();
-        successListener.assertHasResult();
-        sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
-
-        // Starting wifi again will cause TETHER_ERROR_DUPLICATE_REQUEST
-        ResultListener failureListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, failureListener);
-        mLooper.dispatchAll();
-        failureListener.assertHasResult();
-
-        // Stop Wifi tethering.
-        mTethering.stopTethering(TETHERING_WIFI);
-
-        // We should be able to request the same Wifi again
-        ResultListener successListener2 = new ResultListener(TETHER_ERROR_NO_ERROR);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener2);
-        mLooper.dispatchAll();
-        successListener2.assertHasResult();
-    }
-
-    @Test
-    public void testStopTetheringWithMatchingRequest() throws Exception {
-        assumeTrue(isTetheringWithSoftApConfigEnabled());
-        when(mContext.checkCallingOrSelfPermission(NETWORK_SETTINGS)).thenReturn(PERMISSION_DENIED);
-        initTetheringOnTestThread();
-        UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
-        initTetheringUpstream(upstreamState);
-        when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
-
-        // Enable wifi tethering.
-        SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
-                .setSsid("SoftApConfig")
-                .build();
-        final TetheringRequest tetheringRequest = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig).build();
-        tetheringRequest.setUid(TEST_CALLER_UID);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, null);
-        mLooper.dispatchAll();
-
-        // Stop tethering with non-matching config. Should fail with TETHER_ERROR_UNKNOWN_REQUEST.
-        SoftApConfiguration softApConfig2 = new SoftApConfiguration.Builder()
-                .setSsid("SoftApConfig2")
-                .build();
-        IIntResultListener differentConfigListener = mock(IIntResultListener.class);
-        mTethering.stopTetheringRequest(new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig2).build(), differentConfigListener);
-        mLooper.dispatchAll();
-        verify(differentConfigListener).onResult(eq(TETHER_ERROR_UNKNOWN_REQUEST));
-        verify(mWifiManager, never()).stopSoftAp();
-
-        // Stop tethering with non-matching UID. Should fail with TETHER_ERROR_UNKNOWN_REQUEST.
-        final TetheringRequest nonMatchingUid = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig).build();
-        IIntResultListener nonMatchingUidListener = mock(IIntResultListener.class);
-        nonMatchingUid.setUid(TEST_CALLER_UID_2);
-        mTethering.stopTetheringRequest(nonMatchingUid, nonMatchingUidListener);
-        mLooper.dispatchAll();
-        verify(nonMatchingUidListener).onResult(eq(TETHER_ERROR_UNKNOWN_REQUEST));
-        verify(mWifiManager, never()).stopSoftAp();
-
-        // Stop tethering with matching request. Should succeed now.
-        IIntResultListener matchingListener = mock(IIntResultListener.class);
-        mTethering.stopTetheringRequest(tetheringRequest, matchingListener);
-        mLooper.dispatchAll();
-        verify(matchingListener).onResult(eq(TETHER_ERROR_NO_ERROR));
-        verify(mWifiManager).stopSoftAp();
-    }
-
-    @Test
-    public void testStopTetheringWithSettingsPermission() throws Exception {
-        assumeTrue(isTetheringWithSoftApConfigEnabled());
-        initTetheringOnTestThread();
-        UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
-        initTetheringUpstream(upstreamState);
-        when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
-
-        // Enable wifi tethering.
-        SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
-                .setSsid("SoftApConfig")
-                .build();
-        final TetheringRequest tetheringRequest = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig).build();
-        tetheringRequest.setUid(TEST_CALLER_UID);
-        mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, null);
-        mLooper.dispatchAll();
-
-        // Stop tethering with non-matching UID and Settings permission. Should succeed.
-        final TetheringRequest nonMatchingUid = new TetheringRequest.Builder(TETHERING_WIFI)
-                .setSoftApConfiguration(softApConfig).build();
-        IIntResultListener nonMatchingUidListener = mock(IIntResultListener.class);
-        nonMatchingUid.setUid(TEST_CALLER_UID_2);
-        when(mContext.checkCallingOrSelfPermission(NETWORK_SETTINGS))
-                .thenReturn(PERMISSION_GRANTED);
-        mTethering.stopTetheringRequest(nonMatchingUid, nonMatchingUidListener);
-        mLooper.dispatchAll();
-        verify(nonMatchingUidListener).onResult(eq(TETHER_ERROR_NO_ERROR));
-        verify(mWifiManager).stopSoftAp();
-    }
-
     @Test
     public void testReportFailCallbackIfOffloadNotSupported() throws Exception {
         initTetheringOnTestThread();
@@ -3125,8 +2794,7 @@
     }
 
     @Test
-    public void testMultipleStartTetheringLegacy() throws Exception {
-        assumeFalse(isTetheringWithSoftApConfigEnabled());
+    public void testMultipleStartTethering() throws Exception {
         initTetheringOnTestThread();
         final LinkAddress serverLinkAddr = new LinkAddress("192.168.20.1/24");
         final LinkAddress clientLinkAddr = new LinkAddress("192.168.20.42/24");
@@ -3907,7 +3575,6 @@
     @Test
     public void testStartBluetoothTetheringFailsWhenTheresAnExistingRequestWaitingForPanService()
             throws Exception {
-        assumeFalse(isTetheringWithSoftApConfigEnabled());
         initTetheringOnTestThread();
 
         mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 5d99b74..3d7ea69 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -1242,6 +1242,22 @@
     @ConnectivityManagerFeature
     private Long mEnabledConnectivityManagerFeatures = null;
 
+    /**
+     * A class to help with mocking ConnectivityManager.
+     * @hide
+     */
+    public static class MockHelpers {
+        /**
+         * Produce an instance of the class returned by
+         * {@link ConnectivityManager#registerNetworkAgent}
+         * @hide
+         */
+        public static Network registerNetworkAgentResult(
+                @Nullable final Network network, @Nullable final INetworkAgentRegistry registry) {
+            return network;
+        }
+    }
+
     private TetheringManager getTetheringManager() {
         synchronized (mTetheringEventCallbacks) {
             if (mTetheringManager == null) {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index fb42c03..41b58fa 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -33,8 +33,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Collection;
 import java.util.Optional;
 
 /** Helper class to download certificate transparency log files. */
@@ -48,28 +47,21 @@
     private final DownloadHelper mDownloadHelper;
     private final SignatureVerifier mSignatureVerifier;
     private final CertificateTransparencyLogger mLogger;
-
-    private final List<CompatibilityVersion> mCompatVersions = new ArrayList<>();
+    private final Collection<CompatibilityVersion> mCompatVersions;
 
     CertificateTransparencyDownloader(
             Context context,
             DataStore dataStore,
             DownloadHelper downloadHelper,
             SignatureVerifier signatureVerifier,
-            CertificateTransparencyLogger logger) {
+            CertificateTransparencyLogger logger,
+            Collection<CompatibilityVersion> compatVersions) {
         mContext = context;
         mSignatureVerifier = signatureVerifier;
         mDataStore = dataStore;
         mDownloadHelper = downloadHelper;
         mLogger = logger;
-    }
-
-    void addCompatibilityVersion(CompatibilityVersion compatVersion) {
-        mCompatVersions.add(compatVersion);
-    }
-
-    void clearCompatibilityVersions() {
-        mCompatVersions.clear();
+        mCompatVersions = compatVersions;
     }
 
     long startPublicKeyDownload() {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
index f1b9a4f..286f326 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -28,6 +28,8 @@
 import android.os.SystemClock;
 import android.util.Log;
 
+import java.util.Collection;
+
 /** Implementation of the Certificate Transparency job */
 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
 public class CertificateTransparencyJob extends BroadcastReceiver {
@@ -37,8 +39,8 @@
     private final Context mContext;
     private final DataStore mDataStore;
     private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
-    private final CompatibilityVersion mCompatVersion;
     private final SignatureVerifier mSignatureVerifier;
+    private final Collection<CompatibilityVersion> mCompatVersions;
     private final AlarmManager mAlarmManager;
     private final PendingIntent mPendingIntent;
 
@@ -50,13 +52,13 @@
             Context context,
             DataStore dataStore,
             CertificateTransparencyDownloader certificateTransparencyDownloader,
-            CompatibilityVersion compatVersion,
-            SignatureVerifier signatureVerifier) {
+            SignatureVerifier signatureVerifier,
+            Collection<CompatibilityVersion> compatVersions) {
         mContext = context;
         mDataStore = dataStore;
         mCertificateTransparencyDownloader = certificateTransparencyDownloader;
-        mCompatVersion = compatVersion;
         mSignatureVerifier = signatureVerifier;
+        mCompatVersions = compatVersions;
 
         mAlarmManager = context.getSystemService(AlarmManager.class);
         mPendingIntent =
@@ -99,7 +101,9 @@
         }
         mDependenciesReady = false;
 
-        mCompatVersion.delete();
+        for (CompatibilityVersion compatVersion : mCompatVersions) {
+            compatVersion.delete();
+        }
 
         if (Config.DEBUG) {
             Log.d(TAG, "CertificateTransparencyJob canceled.");
@@ -129,7 +133,6 @@
 
     private void startDependencies() {
         mDataStore.load();
-        mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
         mSignatureVerifier.loadAllowedKeys();
         mContext.registerReceiver(
                 mCertificateTransparencyDownloader,
@@ -144,7 +147,6 @@
     private void stopDependencies() {
         mContext.unregisterReceiver(mCertificateTransparencyDownloader);
         mSignatureVerifier.clearAllowedKeys();
-        mCertificateTransparencyDownloader.clearCompatibilityVersions();
         mDataStore.delete();
 
         if (Config.DEBUG) {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index 2e910b2..5e530c7 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -30,6 +30,8 @@
 
 import com.android.server.SystemService;
 
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.concurrent.Executors;
 
 /** Implementation of the Certificate Transparency service. */
@@ -51,8 +53,18 @@
     /** Creates a new {@link CertificateTransparencyService} object. */
     public CertificateTransparencyService(Context context) {
         DataStore dataStore = new DataStore(Config.PREFERENCES_FILE);
-
         SignatureVerifier signatureVerifier = new SignatureVerifier(context);
+        Collection<CompatibilityVersion> compatVersions =
+                Arrays.asList(
+                        new CompatibilityVersion(
+                                Config.COMPATIBILITY_VERSION_V1,
+                                Config.URL_SIGNATURE_V1,
+                                Config.URL_LOG_LIST_V1),
+                        new CompatibilityVersion(
+                                Config.COMPATIBILITY_VERSION_V2,
+                                Config.URL_SIGNATURE_V2,
+                                Config.URL_LOG_LIST_V2));
+
         mCertificateTransparencyJob =
                 new CertificateTransparencyJob(
                         context,
@@ -62,13 +74,10 @@
                                 dataStore,
                                 new DownloadHelper(context),
                                 signatureVerifier,
-                                new CertificateTransparencyLoggerImpl(dataStore)),
-                        new CompatibilityVersion(
-                                Config.COMPATIBILITY_VERSION,
-                                Config.URL_SIGNATURE,
-                                Config.URL_LOG_LIST,
-                                Config.CT_ROOT_DIRECTORY_PATH),
-                        signatureVerifier);
+                                new CertificateTransparencyLoggerImpl(dataStore),
+                                compatVersions),
+                        signatureVerifier,
+                        compatVersions);
     }
 
     /**
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
index e8a6e64..0a91963 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -23,6 +23,8 @@
 import android.system.Os;
 import android.util.Log;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
 
 import org.json.JSONException;
@@ -40,6 +42,8 @@
 
     private static final String TAG = "CompatibilityVersion";
 
+    private static File sRootDirectory = new File(Config.CT_ROOT_DIRECTORY_PATH);
+
     static final String LOGS_DIR_PREFIX = "logs-";
     static final String LOGS_LIST_FILE_NAME = "log_list.json";
     static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
@@ -48,23 +52,21 @@
 
     private final String mMetadataUrl;
     private final String mContentUrl;
-    private final File mRootDirectory;
     private final File mVersionDirectory;
     private final File mCurrentLogsDirSymlink;
 
     CompatibilityVersion(
-            String compatVersion, String metadataUrl, String contentUrl, File rootDirectory) {
+            String compatVersion, String metadataUrl, String contentUrl) {
         mCompatVersion = compatVersion;
         mMetadataUrl = metadataUrl;
         mContentUrl = contentUrl;
-        mRootDirectory = rootDirectory;
-        mVersionDirectory = new File(rootDirectory, compatVersion);
+        mVersionDirectory = new File(sRootDirectory, compatVersion);
         mCurrentLogsDirSymlink = new File(mVersionDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
     }
 
-    CompatibilityVersion(
-            String compatVersion, String metadataUrl, String contentUrl, String rootDirectoryPath) {
-        this(compatVersion, metadataUrl, contentUrl, new File(rootDirectoryPath));
+    @VisibleForTesting
+    static void setRootDirectoryForTesting(File rootDirectory) {
+        sRootDirectory = rootDirectory;
     }
 
     /**
@@ -75,8 +77,8 @@
      * @return true if the log list was installed successfully, false otherwise.
      * @throws IOException if the list cannot be saved in the CT directory.
      */
-    LogListUpdateStatus install(
-            InputStream newContent, LogListUpdateStatus.Builder statusBuilder) throws IOException {
+    LogListUpdateStatus install(InputStream newContent, LogListUpdateStatus.Builder statusBuilder)
+            throws IOException {
         String content = new String(newContent.readAllBytes(), UTF_8);
         try {
             JSONObject contentJson = new JSONObject(content);
@@ -98,7 +100,7 @@
         // there's a bunch of steps. We create a new directory with the logs and then do
         // an atomic update of the current symlink to point to the new directory.
         // 1. Ensure the path to the root and version directories exist and are readable.
-        DirectoryUtils.makeDir(mRootDirectory);
+        DirectoryUtils.makeDir(sRootDirectory);
         DirectoryUtils.makeDir(mVersionDirectory);
 
         File newLogsDir = new File(mVersionDirectory, LOGS_DIR_PREFIX + version);
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index 5fdba09..72b715a 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -33,18 +33,14 @@
     private static final String PREFERENCES_FILE_NAME = "ct.preferences";
     static final File PREFERENCES_FILE = new File(DEVICE_PROTECTED_DATA_DIR, PREFERENCES_FILE_NAME);
 
-    // CT directory
+    // CT paths
     static final String CT_ROOT_DIRECTORY_PATH = "/data/misc/keychain/ct/";
-    static final String COMPATIBILITY_VERSION = "v1";
+    static final String URL_PREFIX = "https://www.gstatic.com/android/certificate_transparency/";
 
     // Phenotype flags
     static final String NAMESPACE_NETWORK_SECURITY = "network_security";
     private static final String FLAGS_PREFIX = "CertificateTransparencyLogList__";
     static final String FLAG_SERVICE_ENABLED = FLAGS_PREFIX + "service_enabled";
-    static final String FLAG_CONTENT_URL = FLAGS_PREFIX + "content_url";
-    static final String FLAG_METADATA_URL = FLAGS_PREFIX + "metadata_url";
-    static final String FLAG_VERSION = FLAGS_PREFIX + "version";
-    static final String FLAG_PUBLIC_KEY = FLAGS_PREFIX + "public_key";
 
     // properties
     static final String VERSION = "version";
@@ -53,9 +49,18 @@
     static final String PUBLIC_KEY_DOWNLOAD_ID = "public_key_download_id";
     static final String LOG_LIST_UPDATE_FAILURE_COUNT = "log_list_update_failure_count";
 
-    // URLs
-    static final String URL_PREFIX = "https://www.gstatic.com/android/certificate_transparency/";
-    static final String URL_LOG_LIST = URL_PREFIX + "log_list.json";
-    static final String URL_SIGNATURE = URL_PREFIX + "log_list.sig";
+    // Public Key URLs
     static final String URL_PUBLIC_KEY = URL_PREFIX + "log_list.pub";
+
+    // Compatibility Version v1
+    static final String COMPATIBILITY_VERSION_V1 = "v1";
+    static final String URL_PREFIX_V1 = URL_PREFIX;
+    static final String URL_LOG_LIST_V1 = URL_PREFIX_V1 + "log_list.json";
+    static final String URL_SIGNATURE_V1 = URL_PREFIX_V1 + "log_list.sig";
+
+    // Compatibility Version v2
+    static final String COMPATIBILITY_VERSION_V2 = "v2";
+    static final String URL_PREFIX_V2 = URL_PREFIX + COMPATIBILITY_VERSION_V2 + "/";
+    static final String URL_LOG_LIST_V2 = URL_PREFIX_V2 + "log_list.json";
+    static final String URL_SIGNATURE_V2 = URL_PREFIX_V2 + "log_list.sig";
 }
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 22dc6ab..956bad5 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -60,6 +60,7 @@
 import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.Signature;
+import java.util.Arrays;
 import java.util.Base64;
 import java.util.Optional;
 
@@ -94,24 +95,25 @@
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
         mDataStore = new DataStore(File.createTempFile("datastore-test", ".properties"));
         mSignatureVerifier = new SignatureVerifier(mContext);
+
+        CompatibilityVersion.setRootDirectoryForTesting(mContext.getFilesDir());
+        mCompatVersion =
+                new CompatibilityVersion(
+                        /* compatVersion= */ "v666",
+                        Config.URL_SIGNATURE_V1,
+                        Config.URL_LOG_LIST_V1);
         mCertificateTransparencyDownloader =
                 new CertificateTransparencyDownloader(
                         mContext,
                         mDataStore,
                         new DownloadHelper(mDownloadManager),
                         mSignatureVerifier,
-                        mLogger);
-        mCompatVersion =
-                new CompatibilityVersion(
-                        /* compatVersion= */ "v666",
-                        Config.URL_SIGNATURE,
-                        Config.URL_LOG_LIST,
-                        mContext.getFilesDir());
+                        mLogger,
+                        Arrays.asList(mCompatVersion));
 
         prepareDownloadManager();
         mSignatureVerifier.addAllowedKey(mPublicKey);
         mDataStore.load();
-        mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
     }
 
     @After
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
index 2b8b3cd..0d15183 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
@@ -27,6 +27,7 @@
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -47,9 +48,16 @@
 
     private final File mTestDir =
             InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
-    private final CompatibilityVersion mCompatVersion =
-            new CompatibilityVersion(
-                    TEST_VERSION, Config.URL_SIGNATURE, Config.URL_LOG_LIST, mTestDir);
+
+    private CompatibilityVersion mCompatVersion;
+
+    @Before
+    public void setUp() {
+        CompatibilityVersion.setRootDirectoryForTesting(mTestDir);
+        mCompatVersion =
+                new CompatibilityVersion(
+                        TEST_VERSION, Config.URL_SIGNATURE_V1, Config.URL_LOG_LIST_V1);
+    }
 
     @After
     public void tearDown() {
@@ -111,9 +119,7 @@
         JSONObject logList = makeLogList(version, "i_am_content");
 
         try (InputStream inputStream = asStream(logList)) {
-            assertThat(
-                            mCompatVersion.install(
-                                    inputStream, LogListUpdateStatus.builder()))
+            assertThat(mCompatVersion.install(inputStream, LogListUpdateStatus.builder()))
                     .isEqualTo(getSuccessfulUpdateStatus());
         }
 
@@ -142,9 +148,7 @@
     @Test
     public void testCompatibilityVersion_deleteSuccessfully() throws Exception {
         try (InputStream inputStream = asStream(makeLogList(/* version= */ "123"))) {
-            assertThat(
-                            mCompatVersion.install(
-                                    inputStream, LogListUpdateStatus.builder()))
+            assertThat(mCompatVersion.install(inputStream, LogListUpdateStatus.builder()))
                     .isEqualTo(getSuccessfulUpdateStatus());
         }
 
@@ -156,9 +160,7 @@
     @Test
     public void testCompatibilityVersion_invalidLogList() throws Exception {
         try (InputStream inputStream = new ByteArrayInputStream(("not_a_valid_list".getBytes()))) {
-            assertThat(
-                            mCompatVersion.install(
-                                    inputStream, LogListUpdateStatus.builder()))
+            assertThat(mCompatVersion.install(inputStream, LogListUpdateStatus.builder()))
                     .isEqualTo(LogListUpdateStatus.builder().setState(LOG_LIST_INVALID).build());
         }
 
@@ -179,9 +181,7 @@
 
         JSONObject newLogList = makeLogList(existingVersion, "i_am_the_real_content");
         try (InputStream inputStream = asStream(newLogList)) {
-            assertThat(
-                            mCompatVersion.install(
-                                    inputStream, LogListUpdateStatus.builder()))
+            assertThat(mCompatVersion.install(inputStream, LogListUpdateStatus.builder()))
                     .isEqualTo(getSuccessfulUpdateStatus());
         }
 
@@ -193,16 +193,12 @@
         String existingVersion = "666";
         JSONObject existingLogList = makeLogList(existingVersion, "i_was_installed_successfully");
         try (InputStream inputStream = asStream(existingLogList)) {
-            assertThat(
-                            mCompatVersion.install(
-                                    inputStream, LogListUpdateStatus.builder()))
+            assertThat(mCompatVersion.install(inputStream, LogListUpdateStatus.builder()))
                     .isEqualTo(getSuccessfulUpdateStatus());
         }
 
         try (InputStream inputStream = asStream(makeLogList(existingVersion, "i_am_ignored"))) {
-            assertThat(
-                            mCompatVersion.install(
-                                    inputStream, LogListUpdateStatus.builder()))
+            assertThat(mCompatVersion.install(inputStream, LogListUpdateStatus.builder()))
                     .isEqualTo(
                             LogListUpdateStatus.builder()
                                     .setState(VERSION_ALREADY_EXISTS)
diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java
index 5f66f47..5ff708d 100644
--- a/service-t/src/com/android/server/net/NetworkStatsFactory.java
+++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java
@@ -21,7 +21,6 @@
 import static android.net.NetworkStats.UID_ALL;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.Context;
 import android.net.NetworkStats;
 import android.net.UnderlyingNetworkInfo;
@@ -31,6 +30,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.BpfNetMaps;
+import com.android.server.connectivity.InterfaceTracker;
 
 import java.io.IOException;
 import java.net.ProtocolException;
@@ -108,7 +108,7 @@
 
         /** Create a new {@link BpfNetMaps}. */
         public BpfNetMaps createBpfNetMaps(@NonNull Context ctx) {
-            return new BpfNetMaps(ctx);
+            return new BpfNetMaps(ctx, new InterfaceTracker(ctx));
         }
     }
 
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 36c0cf9..25c0617 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -49,6 +49,8 @@
 import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED;
 import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.StatsManager;
 import android.content.Context;
 import android.net.BpfNetMapsUtils;
@@ -85,12 +87,14 @@
 import com.android.net.module.util.bpf.IngressDiscardKey;
 import com.android.net.module.util.bpf.IngressDiscardValue;
 import com.android.net.module.util.bpf.LocalNetAccessKey;
+import com.android.server.connectivity.InterfaceTracker;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.StringJoiner;
 
@@ -142,6 +146,7 @@
             Pair.create(TRAFFIC_PERMISSION_INTERNET, "PERMISSION_INTERNET"),
             Pair.create(TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
     );
+    private final InterfaceTracker mInterfaceTracker;
 
     /**
      * Set configurationMap for test.
@@ -423,23 +428,27 @@
 
     /** Constructor used after T that doesn't need to use netd anymore. */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public BpfNetMaps(final Context context) {
-        this(context, null);
+    public BpfNetMaps(final Context context, @NonNull final InterfaceTracker interfaceTracker) {
+        this(context, null, interfaceTracker);
 
         if (!SdkLevel.isAtLeastT()) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
     }
 
-    public BpfNetMaps(final Context context, final INetd netd) {
-        this(context, netd, new Dependencies());
+    public BpfNetMaps(final Context context, final INetd netd, @NonNull final InterfaceTracker
+            interfaceTracker) {
+        this(context, netd, new Dependencies(), interfaceTracker);
     }
 
     @VisibleForTesting
-    public BpfNetMaps(final Context context, final INetd netd, final Dependencies deps) {
+    public BpfNetMaps(final Context context, final INetd netd, final Dependencies deps,
+            @NonNull final  InterfaceTracker interfaceTracker) {
+        Objects.requireNonNull(interfaceTracker);
         if (SdkLevel.isAtLeastT()) {
             ensureInitialized(context);
         }
         mNetd = netd;
         mDeps = deps;
+        mInterfaceTracker = interfaceTracker;
     }
 
     private void maybeThrow(final int err, final String msg) {
@@ -902,7 +911,7 @@
      * @param isAllowed is the local network call allowed or blocked.
      */
     @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
-    public void addLocalNetAccess(final int lpmBitlen, final String iface,
+    public void addLocalNetAccess(final int lpmBitlen, @Nullable final String iface,
             final InetAddress address, final int protocol, final int remotePort,
             final boolean isAllowed) {
         throwIfPre25Q2("addLocalNetAccess is not available on pre-B devices");
@@ -910,7 +919,7 @@
         if (iface == null) {
             ifIndex = 0;
         } else {
-            ifIndex = mDeps.getIfIndex(iface);
+            ifIndex = mInterfaceTracker.getInterfaceIndex(iface);
         }
         if (ifIndex == 0) {
             Log.e(TAG, "Failed to get if index, skip addLocalNetAccess for " + address
@@ -937,14 +946,14 @@
      * @param remotePort src/dst port for ingress/egress
      */
     @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
-    public void removeLocalNetAccess(final int lpmBitlen, final String iface,
+    public void removeLocalNetAccess(final int lpmBitlen, @Nullable final String iface,
             final InetAddress address, final int protocol, final int remotePort) {
         throwIfPre25Q2("removeLocalNetAccess is not available on pre-B devices");
         final int ifIndex;
         if (iface == null) {
             ifIndex = 0;
         } else {
-            ifIndex = mDeps.getIfIndex(iface);
+            ifIndex = mInterfaceTracker.getInterfaceIndex(iface);
         }
         if (ifIndex == 0) {
             Log.e(TAG, "Failed to get if index, skip removeLocalNetAccess for " + address
@@ -973,14 +982,14 @@
      * is not local network or if configuration is allowed like local dns servers.
      */
     @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
-    public boolean getLocalNetAccess(final int lpmBitlen, final String iface,
+    public boolean getLocalNetAccess(final int lpmBitlen, @Nullable final String iface,
             final InetAddress address, final int protocol, final int remotePort) {
         throwIfPre25Q2("getLocalNetAccess is not available on pre-B devices");
         final int ifIndex;
         if (iface == null) {
             ifIndex = 0;
         } else {
-            ifIndex = mDeps.getIfIndex(iface);
+            ifIndex = mInterfaceTracker.getInterfaceIndex(iface);
         }
         if (ifIndex == 0) {
             Log.e(TAG, "Failed to get if index, returning default from getLocalNetAccess for "
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index b2e49e7..3ce3f02 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -365,6 +365,7 @@
 import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
 import com.android.server.connectivity.DscpPolicyTracker;
 import com.android.server.connectivity.FullScore;
+import com.android.server.connectivity.InterfaceTracker;
 import com.android.server.connectivity.InvalidTagException;
 import com.android.server.connectivity.KeepaliveResourceUtil;
 import com.android.server.connectivity.KeepaliveTracker;
@@ -577,6 +578,7 @@
     private final NetworkStatsManager mStatsManager;
     private final NetworkPolicyManager mPolicyManager;
     private final BpfNetMaps mBpfNetMaps;
+    private final InterfaceTracker mInterfaceTracker;
 
     /**
      * TestNetworkService (lazily) created upon first usage. Locked to prevent creation of multiple
@@ -1662,8 +1664,17 @@
          * @param netd a netd binder
          * @return BpfNetMaps implementation.
          */
-        public BpfNetMaps getBpfNetMaps(Context context, INetd netd) {
-            return new BpfNetMaps(context, netd);
+        public BpfNetMaps getBpfNetMaps(Context context, INetd netd,
+                InterfaceTracker interfaceTracker) {
+            return new BpfNetMaps(context, netd, interfaceTracker);
+        }
+
+        /**
+         * Get the InterfaceTracker implementation to use in ConnectivityService.
+         * @return InterfaceTracker implementation.
+         */
+        public InterfaceTracker getInterfaceTracker(Context context) {
+            return new InterfaceTracker(context);
         }
 
         /**
@@ -1886,7 +1897,8 @@
         mWakeUpMask = mask;
 
         mNetd = netd;
-        mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd);
+        mInterfaceTracker = mDeps.getInterfaceTracker(mContext);
+        mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd, mInterfaceTracker);
         mHandlerThread = mDeps.makeHandlerThread("ConnectivityServiceThread");
         mPermissionMonitorDeps = mPermDeps;
         mPermissionMonitor =
@@ -9605,8 +9617,6 @@
 
         updateIngressToVpnAddressFiltering(newLp, oldLp, networkAgent);
 
-        updateLocalNetworkAddresses(newLp, oldLp);
-
         updateMtu(newLp, oldLp);
         // TODO - figure out what to do for clat
 //        for (LinkProperties lp : newLp.getStackedLinks()) {
@@ -9769,16 +9779,23 @@
                     wakeupModifyInterface(iface, nai, true);
                     mDeps.reportNetworkInterfaceForTransports(mContext, iface,
                             nai.networkCapabilities.getTransportTypes());
+                    mInterfaceTracker.addInterface(iface);
                 } catch (Exception e) {
                     logw("Exception adding interface: " + e);
                 }
             }
         }
+
+        // The local network addresses needs to be updated before interfaces are removed because
+        // modifying bpf map local_net_access requires mapping interface name to index.
+        updateLocalNetworkAddresses(newLp, oldLp);
+
         for (final String iface : interfaceDiff.removed) {
             try {
                 if (DBG) log("Removing iface " + iface + " from network " + netId);
                 wakeupModifyInterface(iface, nai, false);
                 mRoutingCoordinatorService.removeInterfaceFromNetwork(netId, iface);
+                mInterfaceTracker.removeInterface(iface);
             } catch (Exception e) {
                 loge("Exception removing interface: " + e);
             }
diff --git a/service/src/com/android/server/connectivity/InterfaceTracker.java b/service/src/com/android/server/connectivity/InterfaceTracker.java
new file mode 100644
index 0000000..0b4abeb
--- /dev/null
+++ b/service/src/com/android/server/connectivity/InterfaceTracker.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.system.Os;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.BpfNetMaps;
+
+import java.util.Map;
+
+/**
+ * InterfaceTracker is responsible for providing interface mapping and tracking.
+ * @hide
+ */
+public class InterfaceTracker {
+    static {
+        if (BpfNetMaps.isAtLeast25Q2()) {
+            System.loadLibrary("service-connectivity");
+        }
+    }
+    private static final String TAG = "InterfaceTracker";
+    private final Dependencies mDeps;
+    private final Map<String, Integer> mInterfaceMap;
+
+    public InterfaceTracker(final Context context) {
+        this(context, new Dependencies());
+    }
+
+    @VisibleForTesting
+    public InterfaceTracker(final Context context, final Dependencies deps) {
+        this.mInterfaceMap = new ArrayMap<>();
+        this.mDeps = deps;
+    }
+
+    /**
+     * To add interface to tracking
+     * @param interfaceName name of interface added.
+     */
+    public void addInterface(@Nullable final String interfaceName) {
+        final int interfaceIndex;
+        if (interfaceName == null) {
+            interfaceIndex = 0;
+        } else {
+            interfaceIndex = mDeps.getIfIndex(interfaceName);
+        }
+        if (interfaceIndex == 0) {
+            Log.e(TAG, "Failed to get interface index for " + interfaceName);
+            return;
+        }
+        synchronized (mInterfaceMap) {
+            mInterfaceMap.put(interfaceName, interfaceIndex);
+        }
+    }
+
+    /**
+     * To remove interface from tracking
+     * @param interfaceName name of interface removed.
+     * @return true if the value was present and now removed.
+     */
+    public boolean removeInterface(@Nullable final String interfaceName) {
+        if (interfaceName == null) return false;
+        synchronized (mInterfaceMap) {
+            return mInterfaceMap.remove(interfaceName) != null;
+        }
+    }
+
+    /**
+     * Get interface index from interface name.
+     * @param interfaceName name of interface
+     * @return interface index for given interface name or 0 if interface is not found.
+     */
+    public int getInterfaceIndex(@Nullable final String interfaceName) {
+        final int interfaceIndex;
+        if (interfaceName != null) {
+            synchronized (mInterfaceMap) {
+                interfaceIndex = mInterfaceMap.getOrDefault(interfaceName, 0);
+            }
+        } else {
+            interfaceIndex = 0;
+        }
+        return interfaceIndex;
+    }
+
+    /**
+     * Dependencies of InterfaceTracker, for injection in tests.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        /**
+         * Get interface index.
+         */
+        public int getIfIndex(final String ifName) {
+            return Os.if_nametoindex(ifName);
+        }
+
+        /**
+         * Get interface name
+         */
+        public String getIfName(final int ifIndex) {
+            return Os.if_indextoname(ifIndex);
+        }
+
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
index c19a124..5d49fa3 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -22,7 +22,6 @@
 import static com.android.net.module.util.netlink.NetlinkConstants.IFF_UP;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_GETLINK;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWLINK;
-import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST_ACK;
 
 import android.net.MacAddress;
@@ -307,7 +306,7 @@
         }
 
         return RtNetlinkLinkMessage.build(
-                new StructNlMsgHdr(0, RTM_GETLINK, NLM_F_REQUEST, sequenceNumber),
+                new StructNlMsgHdr(0, RTM_GETLINK, NLM_F_REQUEST_ACK, sequenceNumber),
                 new StructIfinfoMsg((short) AF_UNSPEC, (short) 0, interfaceIndex, 0, 0),
                 DEFAULT_MTU, null, null);
     }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
index 13710b1..08cab03 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -290,7 +290,7 @@
     @Test
     public void testCreateGetLinkMessage() {
         final String expectedHexBytes =
-                "20000000120001006824000000000000"    // struct nlmsghdr
+                "20000000120005006824000000000000"    // struct nlmsghdr
                 + "00000000080000000000000000000000"; // struct ifinfomsg
         final String interfaceName = "wlan0";
         final int interfaceIndex = 8;
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index 49ffad6..c2ad18e 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -18,6 +18,7 @@
 from mobly.controllers import android_device
 from mobly.controllers.android_device_lib.adb import AdbError
 from net_tests_utils.host.python import adb_utils, assert_utils
+import functools
 
 
 class PatternNotFoundException(Exception):
@@ -400,6 +401,20 @@
       f" {expected_version}",
   )
 
+def at_least_B():
+  def decorator(test_function):
+    @functools.wraps(test_function)
+    def wrapper(self, *args, **kwargs):
+      asserts.abort_class_if(
+        (not hasattr(self, 'client')) or (not hasattr(self.client, 'isAtLeastB')),
+        "client device is not B+"
+      )
+
+      asserts.abort_class_if(not self.client.isAtLeastB(), "not B+")
+      return test_function(self, *args, **kwargs)
+    return wrapper
+  return decorator
+
 class AdbOutputHandler:
   def __init__(self, ad, cmd):
     self._ad = ad
diff --git a/staticlibs/testutils/host/python/multi_devices_test_base.py b/staticlibs/testutils/host/python/multi_devices_test_base.py
index 677329a..72bac0c 100644
--- a/staticlibs/testutils/host/python/multi_devices_test_base.py
+++ b/staticlibs/testutils/host/python/multi_devices_test_base.py
@@ -53,3 +53,4 @@
         raise_on_exception=True,
     )
     self.client = self.clientDevice.connectivity_multi_devices_snippet
+    self.server = self.serverDevice.connectivity_multi_devices_snippet
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index c730b86..00fb934 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -29,6 +29,7 @@
     libs: [
         "absl-py",
         "mobly",
+        "scapy",
         "net-tests-utils-host-python-common",
     ],
     test_suites: [
diff --git a/tests/cts/multidevices/apfv6_test.py b/tests/cts/multidevices/apfv6_test.py
index fc732d2..61f1bfc 100644
--- a/tests/cts/multidevices/apfv6_test.py
+++ b/tests/cts/multidevices/apfv6_test.py
@@ -13,6 +13,8 @@
 #  limitations under the License.
 
 from mobly import asserts
+from scapy.layers.inet import IP, ICMP
+from scapy.layers.l2 import Ether
 from net_tests_utils.host.python import apf_test_base, apf_utils, adb_utils, assert_utils, packet_utils
 
 APFV6_VERSION = 6000
@@ -82,3 +84,18 @@
         self.send_packet_and_expect_reply_received(
             arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
         )
+
+    @apf_utils.at_least_B()
+    def test_ipv4_icmp_echo_request_offload(self):
+        eth = Ether(src=self.server_mac_address, dst=self.client_mac_address)
+        ip = IP(src=self.server_ipv4_addresses[0], dst=self.client_ipv4_addresses[0])
+        icmp = ICMP(id=1, seq=123)
+        echo_request = bytes(eth/ip/icmp/b"hello").hex()
+
+        eth = Ether(src=self.client_mac_address, dst=self.server_mac_address)
+        ip = IP(src=self.client_ipv4_addresses[0], dst=self.server_ipv4_addresses[0])
+        icmp = ICMP(type=0, id=1, seq=123)
+        expected_echo_reply = bytes(eth/ip/icmp/b"hello").hex()
+        self.send_packet_and_expect_reply_received(
+            echo_request, "DROPPED_IPV4_PING_REQUEST_REPLIED", expected_echo_reply
+        )
\ No newline at end of file
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 252052e..e1c6bf1 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -33,6 +33,8 @@
 import android.net.wifi.WifiInfo
 import android.net.wifi.WifiManager
 import android.net.wifi.WifiSsid
+import android.os.Build.VERSION.CODENAME
+import android.os.Build.VERSION.SDK_INT
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.compatibility.common.util.PropertyUtil
 import com.android.modules.utils.build.SdkLevel
@@ -62,6 +64,17 @@
         cbHelper.unregisterAll()
     }
 
+    private fun isAtLeastPreReleaseCodename(codeName: String): Boolean {
+        // Special case "REL", which means the build is not a pre-release build.
+        if ("REL".equals(CODENAME)) {
+            return false
+        }
+
+        // Otherwise lexically compare them. Return true if the build codename is equal to or
+        // greater than the requested codename.
+        return CODENAME.compareTo(codeName) >= 0
+    }
+
     @Rpc(description = "Check whether the device has wifi feature.")
     fun hasWifiFeature() = pm.hasSystemFeature(FEATURE_WIFI)
 
@@ -77,6 +90,11 @@
     @Rpc(description = "Return whether the Sdk level is at least V.")
     fun isAtLeastV() = SdkLevel.isAtLeastV()
 
+    @Rpc(description = "Check whether the device is at least B.")
+    fun isAtLeastB(): Boolean {
+        return SDK_INT >= 36 || (SDK_INT == 35 && isAtLeastPreReleaseCodename("Baklava"))
+    }
+
     @Rpc(description = "Return the API level that the VSR requirement must be fulfilled.")
     fun getVsrApiLevel() = PropertyUtil.getVsrApiLevel()
 
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 8fcc703..5e035a2 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -172,6 +172,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.argThat
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mockito.doReturn
@@ -1066,7 +1067,20 @@
     fun testAgentStartsInConnecting() {
         val mockContext = mock(Context::class.java)
         val mockCm = mock(ConnectivityManager::class.java)
+        val mockedResult = ConnectivityManager.MockHelpers.registerNetworkAgentResult(
+            mock(Network::class.java),
+            mock(INetworkAgentRegistry::class.java)
+        )
         doReturn(mockCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE)
+        doReturn(mockedResult).`when`(mockCm).registerNetworkAgent(
+            any(),
+            any(),
+            any(),
+            any(),
+            any(),
+            any(),
+            anyInt()
+        )
         val agent = createNetworkAgent(mockContext)
         agent.register()
         verify(mockCm).registerNetworkAgent(
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index ed6107b..e645f67 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -32,7 +32,6 @@
 import static android.net.TetheringManager.TETHERING_VIRTUAL;
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
-import static android.net.TetheringManager.TETHER_ERROR_DUPLICATE_REQUEST;
 import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
 import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
@@ -235,30 +234,6 @@
 
     }
 
-    @Test
-    public void testStartTetheringDuplicateRequestRejected() throws Exception {
-        assumeTrue(isTetheringWithSoftApConfigEnabled());
-        final TestTetheringEventCallback tetherEventCallback =
-                mCtsTetheringUtils.registerTetheringEventCallback();
-        try {
-            tetherEventCallback.assumeWifiTetheringSupported(mContext);
-            tetherEventCallback.expectNoTetheringActive();
-
-            final String[] wifiRegexs = mTM.getTetherableWifiRegexs();
-            mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
-            mTetherChangeReceiver.expectTethering(true /* active */, wifiRegexs);
-
-            final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
-            runAsShell(TETHER_PRIVILEGED, () -> {
-                mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
-                        c -> c.run() /* executor */, startTetheringCallback);
-                startTetheringCallback.expectTetheringFailed(TETHER_ERROR_DUPLICATE_REQUEST);
-            });
-        } finally {
-            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
-        }
-    }
-
     private SoftApConfiguration createSoftApConfiguration(@NonNull String ssid) {
         SoftApConfiguration config;
         if (SdkLevel.isAtLeastT()) {
@@ -405,20 +380,17 @@
             tetherEventCallback.assumeWifiTetheringSupported(mContext);
             tetherEventCallback.expectNoTetheringActive();
 
-            SoftApConfiguration softApConfig = isTetheringWithSoftApConfigEnabled()
-                    ? createSoftApConfiguration("SSID") : null;
+            SoftApConfiguration softApConfig = createSoftApConfiguration("SSID");
             final TetheringInterface tetheredIface =
                     mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
 
             assertNotNull(tetheredIface);
-            if  (isTetheringWithSoftApConfigEnabled()) {
-                assertEquals(softApConfig, tetheredIface.getSoftApConfiguration());
-            }
+            assertEquals(softApConfig, tetheredIface.getSoftApConfiguration());
+            final String wifiTetheringIface = tetheredIface.getInterface();
 
             mCtsTetheringUtils.stopWifiTethering(tetherEventCallback);
 
             if (!SdkLevel.isAtLeastB()) {
-                final String wifiTetheringIface = tetheredIface.getInterface();
                 try {
                     final int ret = runAsShell(TETHER_PRIVILEGED,
                             () -> mTM.tether(wifiTetheringIface));
@@ -483,73 +455,25 @@
     }
 
     @Test
-    public void testStopTetheringRequestNoMatchFailure() throws Exception {
-        assumeTrue(isTetheringWithSoftApConfigEnabled());
-        final TestTetheringEventCallback tetherEventCallback =
-                mCtsTetheringUtils.registerTetheringEventCallback();
-        try {
-            final StartTetheringCallback startTetheringCallback = new StartTetheringCallback();
-            mTM.startTethering(new TetheringRequest.Builder(TETHERING_VIRTUAL).build(),
-                    c -> c.run(), startTetheringCallback);
-
-            // Stopping a non-matching request should have no effect
-            TetheringRequest nonMatchingRequest = new TetheringRequest.Builder(TETHERING_VIRTUAL)
-                    .setInterfaceName("iface")
-                    .build();
-            mCtsTetheringUtils.stopTethering(nonMatchingRequest, false /* success */);
-        } finally {
-            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
-        }
-    }
-
-    @Test
-    public void testStopTetheringRequestMatchSuccess() throws Exception {
+    public void testStopTetheringRequest() throws Exception {
         assumeTrue(isTetheringWithSoftApConfigEnabled());
         final TestTetheringEventCallback tetherEventCallback =
                 mCtsTetheringUtils.registerTetheringEventCallback();
         try {
             tetherEventCallback.assumeWifiTetheringSupported(mContext);
 
-            SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
-                    .setWifiSsid(WifiSsid.fromBytes("This is one config"
-                            .getBytes(StandardCharsets.UTF_8))).build();
-            mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
+            // stopTethering without any tethering active should fail.
+            TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+            mCtsTetheringUtils.stopTethering(request, false /* expectSuccess */);
 
-            // Stopping the active request should stop tethering.
-            TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
-                    .setSoftApConfiguration(softApConfig)
-                    .build();
-            mCtsTetheringUtils.stopTethering(request, true /* success */);
+            // Start wifi tethering
+            mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
+
+            // stopTethering should succeed now that there's a request.
+            mCtsTetheringUtils.stopTethering(request, true /* expectSuccess */);
             tetherEventCallback.expectNoTetheringActive();
         } finally {
-            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
-        }
-    }
-
-    @Test
-    public void testStopTetheringRequestFuzzyMatchSuccess() throws Exception {
-        assumeTrue(isTetheringWithSoftApConfigEnabled());
-        final TestTetheringEventCallback tetherEventCallback =
-                mCtsTetheringUtils.registerTetheringEventCallback();
-        try {
-            tetherEventCallback.assumeWifiTetheringSupported(mContext);
-
-            SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
-                    .setWifiSsid(WifiSsid.fromBytes("This is one config"
-                            .getBytes(StandardCharsets.UTF_8))).build();
-            mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
-
-            // Stopping with a fuzzy matching request should stop tethering.
-            final LinkAddress localAddr = new LinkAddress("192.168.24.5/24");
-            final LinkAddress clientAddr = new LinkAddress("192.168.24.100/24");
-            TetheringRequest fuzzyMatchingRequest = new TetheringRequest.Builder(TETHERING_WIFI)
-                    .setSoftApConfiguration(softApConfig)
-                    .setShouldShowEntitlementUi(true)
-                    .setStaticIpv4Addresses(localAddr, clientAddr)
-                    .build();
-            mCtsTetheringUtils.stopTethering(fuzzyMatchingRequest, true /* success */);
-            tetherEventCallback.expectNoTetheringActive();
-        } finally {
+            mCtsTetheringUtils.stopAllTethering();
             mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
         }
     }
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 437eb81..de39215 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -55,11 +55,11 @@
 import com.android.networkstack.apishim.TelephonyManagerShimImpl
 import com.android.server.BpfNetMaps
 import com.android.server.ConnectivityService
-import com.android.server.L2capNetworkProvider
 import com.android.server.NetworkAgentWrapper
 import com.android.server.TestNetIdManager
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator
 import com.android.server.connectivity.ConnectivityResources
+import com.android.server.connectivity.InterfaceTracker
 import com.android.server.connectivity.MockableSystemProperties
 import com.android.server.connectivity.MultinetworkPolicyTracker
 import com.android.server.connectivity.PermissionMonitor
@@ -114,6 +114,8 @@
     @Mock
     private lateinit var netd: INetd
     @Mock
+    private lateinit var interfaceTracker: InterfaceTracker
+    @Mock
     private lateinit var dnsResolver: IDnsResolver
     @Mock
     private lateinit var systemConfigManager: SystemConfigManager
@@ -140,11 +142,15 @@
 
         private val realContext get() = InstrumentationRegistry.getInstrumentation().context
         private val httpProbeUrl get() =
-            realContext.getResources().getString(com.android.server.net.integrationtests.R.string
-                    .config_captive_portal_http_url)
+            realContext.getResources().getString(
+                com.android.server.net.integrationtests.R.string
+                    .config_captive_portal_http_url
+            )
         private val httpsProbeUrl get() =
-            realContext.getResources().getString(com.android.server.net.integrationtests.R.string
-                    .config_captive_portal_https_url)
+            realContext.getResources().getString(
+                com.android.server.net.integrationtests.R.string
+                    .config_captive_portal_https_url
+            )
 
         private class InstrumentationServiceConnection : ServiceConnection {
             override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
@@ -166,12 +172,19 @@
         fun setUpClass() {
             val intent = Intent(realContext, NetworkStackInstrumentationService::class.java)
             intent.action = INetworkStackInstrumentation::class.qualifiedName
-            assertTrue(realContext.bindService(intent, InstrumentationServiceConnection(),
-                    BIND_AUTO_CREATE or BIND_IMPORTANT),
-                    "Error binding to instrumentation service")
-            assertTrue(bindingCondition.block(SERVICE_BIND_TIMEOUT_MS),
+            assertTrue(
+                realContext.bindService(
+                    intent,
+                    InstrumentationServiceConnection(),
+                    BIND_AUTO_CREATE or BIND_IMPORTANT
+                ),
+                    "Error binding to instrumentation service"
+            )
+            assertTrue(
+                bindingCondition.block(SERVICE_BIND_TIMEOUT_MS),
                     "Timed out binding to instrumentation service " +
-                            "after $SERVICE_BIND_TIMEOUT_MS ms")
+                            "after $SERVICE_BIND_TIMEOUT_MS ms"
+            )
         }
     }
 
@@ -201,7 +214,8 @@
         // We don't test the actual notification value strings, so just return an empty array.
         // It doesn't matter what the values are as long as it's not null.
         doReturn(emptyArray<String>()).`when`(resources).getStringArray(
-                R.array.network_switch_type_name)
+                R.array.network_switch_type_name
+        )
         doReturn(1).`when`(resources).getInteger(R.integer.config_networkAvoidBadWifi)
         doReturn(R.array.config_networkSupportedKeepaliveCount).`when`(resources)
                 .getIdentifier(eq("config_networkSupportedKeepaliveCount"), eq("array"), any())
@@ -223,7 +237,13 @@
     }
 
     private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService(
-            context, dnsResolver, log, netd, deps, PermissionMonitorDependencies())
+            context,
+        dnsResolver,
+        log,
+        netd,
+        deps,
+        PermissionMonitorDependencies()
+    )
 
     private inner class TestDependencies : ConnectivityService.Dependencies() {
         override fun getNetworkStack() = networkStackClient
@@ -231,7 +251,11 @@
             mock(ProxyTracker::class.java)
         override fun getSystemProperties() = mock(MockableSystemProperties::class.java)
         override fun makeNetIdManager() = TestNetIdManager()
-        override fun getBpfNetMaps(context: Context?, netd: INetd?) = mock(BpfNetMaps::class.java)
+        override fun getBpfNetMaps(
+            context: Context?,
+            netd: INetd?,
+                                   interfaceTracker: InterfaceTracker?
+        ) = mock(BpfNetMaps::class.java)
                 .also {
                     doReturn(PERMISSION_INTERNET).`when`(it).getNetPermForUid(anyInt())
                 }
@@ -241,13 +265,17 @@
             c: Context,
             h: Handler,
             r: Runnable
-        ) = MultinetworkPolicyTracker(c, h, r,
+        ) = MultinetworkPolicyTracker(
+            c,
+            h,
+            r,
             object : MultinetworkPolicyTracker.Dependencies() {
                 override fun getResourcesForActiveSubId(
                     connResources: ConnectivityResources,
                     activeSubId: Int
                 ) = resources
-            })
+            }
+        )
 
         override fun makeHandlerThread(tag: String): HandlerThread =
             super.makeHandlerThread(tag).also { handlerThreads.add(it) }
@@ -259,13 +287,18 @@
                 listener: BiConsumer<Int, Int>,
                 handler: Handler
         ): CarrierPrivilegeAuthenticator {
-            return CarrierPrivilegeAuthenticator(context,
+            return CarrierPrivilegeAuthenticator(
+                context,
                     object : CarrierPrivilegeAuthenticator.Dependencies() {
                         override fun makeHandlerThread(): HandlerThread =
                                 super.makeHandlerThread().also { handlerThreads.add(it) }
                     },
-                    tm, TelephonyManagerShimImpl.newInstance(tm),
-                    requestRestrictedWifiEnabled, listener, handler)
+                    tm,
+                TelephonyManagerShimImpl.newInstance(tm),
+                    requestRestrictedWifiEnabled,
+                listener,
+                handler
+            )
         }
 
         override fun makeSatelliteAccessController(
@@ -273,7 +306,8 @@
             updateSatellitePreferredUid: Consumer<MutableSet<Int>>?,
             connectivityServiceInternalHandler: Handler
         ): SatelliteAccessController? = mock(
-            SatelliteAccessController::class.java)
+            SatelliteAccessController::class.java
+        )
 
         override fun makeL2capNetworkProvider(context: Context) = null
     }
@@ -305,8 +339,12 @@
         nsInstrumentation.addHttpResponse(HttpResponse(httpProbeUrl, responseCode = 204))
         nsInstrumentation.addHttpResponse(HttpResponse(httpsProbeUrl, responseCode = 204))
 
-        val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, LinkProperties(), null /* ncTemplate */,
-                context)
+        val na = NetworkAgentWrapper(
+            TRANSPORT_CELLULAR,
+            LinkProperties(),
+            null /* ncTemplate */,
+                context
+        )
         networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
 
         na.addCapability(NET_CAPABILITY_INTERNET)
@@ -344,7 +382,8 @@
                     |  "user-portal-url": "https://login.capport.android.com",
                     |  "venue-info-url": "https://venueinfo.capport.android.com"
                     |}
-                """.trimMargin()))
+                """.trimMargin()
+        ))
 
         // Tests will fail if a non-mocked query is received: mock the HTTPS probe, but not the
         // HTTP probe as it should not be sent.
@@ -398,8 +437,10 @@
                 BpfUtils.BPF_CGROUP_INET_EGRESS,
                 BpfUtils.BPF_CGROUP_INET_SOCK_CREATE
         ).forEach {
-            val ret = SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
-                    "cmd connectivity bpf-get-cgroup-program-id $it").trim()
+            val ret = SystemUtil.runShellCommand(
+                InstrumentationRegistry.getInstrumentation(),
+                    "cmd connectivity bpf-get-cgroup-program-id $it"
+            ).trim()
 
             assertTrue(Integer.parseInt(ret) > 0, "Unexpected output $ret for type $it")
         }
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index caf1765..1d2e8b0 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -109,6 +109,7 @@
 import com.android.net.module.util.bpf.IngressDiscardKey;
 import com.android.net.module.util.bpf.IngressDiscardValue;
 import com.android.net.module.util.bpf.LocalNetAccessKey;
+import com.android.server.connectivity.InterfaceTracker;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -170,6 +171,8 @@
     @Mock INetd mNetd;
     @Mock BpfNetMaps.Dependencies mDeps;
     @Mock Context mContext;
+
+    @Mock InterfaceTracker mInterfaceTracker;
     private final IBpfMap<S32, U32> mConfigurationMap = new TestBpfMap<>(S32.class, U32.class);
     private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap =
             new TestBpfMap<>(S32.class, UidOwnerValue.class);
@@ -188,6 +191,7 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         doReturn(TEST_IF_INDEX).when(mDeps).getIfIndex(TEST_IF_NAME);
+        doReturn(TEST_IF_INDEX).when(mInterfaceTracker).getInterfaceIndex(TEST_IF_NAME);
         doReturn(TEST_IF_NAME).when(mDeps).getIfName(TEST_IF_INDEX);
         doReturn(0).when(mDeps).synchronizeKernelRCU();
         BpfNetMaps.setConfigurationMapForTest(mConfigurationMap);
@@ -202,7 +206,7 @@
         BpfNetMaps.setDataSaverEnabledMapForTest(mDataSaverEnabledMap);
         mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
         BpfNetMaps.setIngressDiscardMapForTest(mIngressDiscardMap);
-        mBpfNetMaps = new BpfNetMaps(mContext, mNetd, mDeps);
+        mBpfNetMaps = new BpfNetMaps(mContext, mNetd, mDeps, mInterfaceTracker);
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 04411dd..5d08807 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -413,6 +413,7 @@
 import com.android.server.connectivity.ClatCoordinator;
 import com.android.server.connectivity.ConnectivityFlags;
 import com.android.server.connectivity.ConnectivityResources;
+import com.android.server.connectivity.InterfaceTracker;
 import com.android.server.connectivity.KeepaliveTracker;
 import com.android.server.connectivity.MultinetworkPolicyTracker;
 import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies;
@@ -2262,7 +2263,8 @@
         }
 
         @Override
-        public BpfNetMaps getBpfNetMaps(Context context, INetd netd) {
+        public BpfNetMaps getBpfNetMaps(Context context, INetd netd,
+                InterfaceTracker interfaceTracker) {
             return mBpfNetMaps;
         }
 
diff --git a/tests/unit/java/com/android/server/connectivity/InterfaceTrackerTest.java b/tests/unit/java/com/android/server/connectivity/InterfaceTrackerTest.java
new file mode 100644
index 0000000..8a9ada0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/InterfaceTrackerTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.ConnectivityModuleTest;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@ConnectivityModuleTest
+public class InterfaceTrackerTest {
+    private static final String TAG = "InterfaceTrackerTest";
+    private static final String TEST_IF_NAME = "wlan10";
+    private static final String TEST_INCORRECT_IF_NAME = "wlan20";
+    private static final int TEST_IF_INDEX = 7;
+
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+    private InterfaceTracker mInterfaceTracker;
+
+    @Mock Context mContext;
+    @Mock InterfaceTracker.Dependencies mDeps;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        doReturn(TEST_IF_INDEX).when(mDeps).getIfIndex(TEST_IF_NAME);
+        mInterfaceTracker = new InterfaceTracker(mContext, mDeps);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testAddingInterface_InterfaceNameIndexMappingAdded() {
+        mInterfaceTracker.addInterface(TEST_IF_NAME);
+        assertEquals(TEST_IF_INDEX, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testAddingNullInterface_InterfaceNameIndexMappingNotAdded() {
+        mInterfaceTracker.addInterface(null);
+        assertEquals(0, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testAddingIncorrectInterface_InterfaceNameIndexMappingNotAdded() {
+        mInterfaceTracker.addInterface(TEST_INCORRECT_IF_NAME);
+
+        assertEquals(0, mInterfaceTracker.getInterfaceIndex(TEST_INCORRECT_IF_NAME));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemovingInterface_InterfaceNameIndexMappingRemoved() {
+        mInterfaceTracker.addInterface(TEST_IF_NAME);
+        assertEquals(TEST_IF_INDEX, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+        mInterfaceTracker.removeInterface(TEST_IF_NAME);
+        assertEquals(0, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemovingNullInterface_InterfaceNameIndexMappingNotRemoved() {
+        mInterfaceTracker.addInterface(TEST_IF_NAME);
+        mInterfaceTracker.removeInterface(null);
+        assertEquals(TEST_IF_INDEX, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void testRemovingIncorrectInterface_InterfaceNameIndexMappingNotRemoved() {
+        mInterfaceTracker.addInterface(TEST_IF_NAME);
+        mInterfaceTracker.removeInterface(TEST_INCORRECT_IF_NAME);
+        assertEquals(TEST_IF_INDEX, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+    }
+
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 557bfd6..d7e781e 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -68,6 +68,7 @@
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator
 import com.android.server.connectivity.ClatCoordinator
 import com.android.server.connectivity.ConnectivityFlags
+import com.android.server.connectivity.InterfaceTracker
 import com.android.server.connectivity.MulticastRoutingCoordinatorService
 import com.android.server.connectivity.MultinetworkPolicyTracker
 import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
@@ -193,6 +194,7 @@
     val connResources = makeMockConnResources(sysResources, packageManager)
 
     val netd = mock<INetd>()
+    val interfaceTracker = mock<InterfaceTracker>()
     val bpfNetMaps = mock<BpfNetMaps>().also {
         doReturn(PERMISSION_INTERNET).`when`(it).getNetPermForUid(anyInt())
     }
@@ -279,7 +281,11 @@
 
     inner class CSDeps : ConnectivityService.Dependencies() {
         override fun getResources(ctx: Context) = connResources
-        override fun getBpfNetMaps(context: Context, netd: INetd) = this@CSTest.bpfNetMaps
+        override fun getBpfNetMaps(
+            context: Context,
+            netd: INetd,
+            interfaceTracker: InterfaceTracker
+        ) = this@CSTest.bpfNetMaps
         override fun getClatCoordinator(netd: INetd?) = this@CSTest.clatCoordinator
         override fun getNetworkStack() = this@CSTest.networkStack
 
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index 520a434..2f2a5d1 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -326,12 +326,13 @@
     private static LinkAddress newLinkAddress(
             Ipv6AddressInfo addressInfo, boolean hasActiveOmrAddress) {
         // Mesh-local addresses and OMR address have the same scope, to distinguish them we set
-        // mesh-local addresses as deprecated when there is an active OMR address.
+        // mesh-local addresses as deprecated when there is an active OMR address. If OMR address
+        // is missing, only ML-EID in mesh-local addresses will be set preferred.
         // For OMR address and link-local address we only use the value isPreferred set by
         // ot-daemon.
         boolean isPreferred = addressInfo.isPreferred;
-        if (addressInfo.isMeshLocal && hasActiveOmrAddress) {
-            isPreferred = false;
+        if (addressInfo.isMeshLocal) {
+            isPreferred = (!hasActiveOmrAddress && addressInfo.isMeshLocalEid);
         }
 
         final long deprecationTimeMillis =
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
index 5ba76b8..5be8f49 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
@@ -19,12 +19,10 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assume.assumeFalse;
-import static org.junit.Assume.assumeNotNull;
 import static org.junit.Assume.assumeTrue;
 
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.net.thread.ThreadNetworkController;
 import android.net.thread.ThreadNetworkManager;
 import android.os.Build;
 
@@ -41,8 +39,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.List;
-
 /** Tests for {@link ThreadNetworkManager}. */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index 195f6d2..5b07e0a 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -22,12 +22,14 @@
 import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
 import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
 import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
+import static android.net.thread.utils.IntegrationTestUtils.getIpv6Addresses;
 import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
 import static android.net.thread.utils.IntegrationTestUtils.getPrefixesFromNetData;
 import static android.net.thread.utils.IntegrationTestUtils.getThreadNetwork;
 import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
 import static android.net.thread.utils.IntegrationTestUtils.waitFor;
 import static android.net.thread.utils.ThreadNetworkControllerWrapper.JOIN_TIMEOUT;
+import static android.os.SystemClock.elapsedRealtime;
 
 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
@@ -38,6 +40,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -46,23 +49,26 @@
 import android.net.ConnectivityManager;
 import android.net.InetAddresses;
 import android.net.IpPrefix;
-import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.thread.utils.FullThreadDevice;
 import android.net.thread.utils.OtDaemonController;
+import android.net.thread.utils.TapTestNetworkTracker;
 import android.net.thread.utils.ThreadFeatureCheckerRule;
 import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice;
 import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
 import android.net.thread.utils.ThreadNetworkControllerWrapper;
 import android.net.thread.utils.ThreadStateListener;
+import android.os.HandlerThread;
 import android.os.SystemClock;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.LargeTest;
 
+import com.google.common.collect.FluentIterable;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -102,6 +108,12 @@
 
     private static final Duration NETWORK_CALLBACK_TIMEOUT = Duration.ofSeconds(10);
 
+    // The duration between attached and OMR address shows up on thread-wpan
+    private static final Duration OMR_LINK_ADDR_TIMEOUT = Duration.ofSeconds(30);
+
+    // The duration between attached and addresses show up on thread-wpan
+    private static final Duration LINK_ADDR_TIMEOUT = Duration.ofSeconds(2);
+
     // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
     private static final byte[] DEFAULT_DATASET_TLVS =
             base16().decode(
@@ -128,6 +140,8 @@
             ThreadNetworkControllerWrapper.newInstance(mContext);
     private OtDaemonController mOtCtl;
     private FullThreadDevice mFtd;
+    private HandlerThread mHandlerThread;
+    private TapTestNetworkTracker mTestNetworkTracker;
 
     public final boolean mIsBorderRouterEnabled;
     private final ThreadConfiguration mConfig;
@@ -152,6 +166,14 @@
         mController.setEnabledAndWait(true);
         mController.setConfigurationAndWait(mConfig);
         mController.leaveAndWait();
+
+        mHandlerThread = new HandlerThread("ThreadIntegrationTest");
+        mHandlerThread.start();
+
+        mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
+        assertThat(mTestNetworkTracker).isNotNull();
+        mController.setTestNetworkAsUpstreamAndWait(mTestNetworkTracker.getInterfaceName());
+
         mFtd = new FullThreadDevice(10 /* nodeId */);
     }
 
@@ -159,6 +181,13 @@
     public void tearDown() throws Exception {
         ThreadStateListener.stopAllListeners();
 
+        if (mTestNetworkTracker != null) {
+            mTestNetworkTracker.tearDown();
+        }
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
         mController.setTestNetworkAsUpstreamAndWait(null);
         mController.leaveAndWait();
 
@@ -265,23 +294,51 @@
     }
 
     @Test
-    public void joinNetworkWithBrDisabled_meshLocalAddressesArePreferred() throws Exception {
-        // When BR feature is disabled, there is no OMR address, so the mesh-local addresses are
-        // expected to be preferred.
-        mOtCtl.executeCommand("br disable");
+    public void joinNetwork_borderRouterEnabled_allMlAddrAreNotPreferredAndOmrIsPreferred()
+            throws Exception {
+        assumeTrue(mConfig.isBorderRouterEnabled());
+
+        mController.setTestNetworkAsUpstreamAndWait(mTestNetworkTracker.getInterfaceName());
         mController.joinAndWait(DEFAULT_DATASET);
+        waitFor(
+                () -> getIpv6Addresses("thread-wpan").contains(mOtCtl.getOmrAddress()),
+                OMR_LINK_ADDR_TIMEOUT);
 
         IpPrefix meshLocalPrefix = DEFAULT_DATASET.getMeshLocalPrefix();
-        List<LinkAddress> linkAddresses = getIpv6LinkAddresses("thread-wpan");
-        for (LinkAddress address : linkAddresses) {
-            if (meshLocalPrefix.contains(address.getAddress())) {
-                assertThat(address.getDeprecationTime())
-                        .isGreaterThan(SystemClock.elapsedRealtime());
-                assertThat(address.isPreferred()).isTrue();
-            }
-        }
+        var linkAddrs = FluentIterable.from(getIpv6LinkAddresses("thread-wpan"));
+        var meshLocalAddrs = linkAddrs.filter(addr -> meshLocalPrefix.contains(addr.getAddress()));
+        assertThat(meshLocalAddrs).isNotEmpty();
+        assertThat(meshLocalAddrs.allMatch(addr -> !addr.isPreferred())).isTrue();
+        assertThat(meshLocalAddrs.allMatch(addr -> addr.getDeprecationTime() <= elapsedRealtime()))
+                .isTrue();
+        var omrAddrs = linkAddrs.filter(addr -> addr.getAddress().equals(mOtCtl.getOmrAddress()));
+        assertThat(omrAddrs).hasSize(1);
+        assertThat(omrAddrs.get(0).isPreferred()).isTrue();
+        assertThat(omrAddrs.get(0).getDeprecationTime() > elapsedRealtime()).isTrue();
+    }
 
-        mOtCtl.executeCommand("br enable");
+    @Test
+    public void joinNetwork_borderRouterDisabled_onlyMlEidIsPreferred() throws Exception {
+        assumeFalse(mConfig.isBorderRouterEnabled());
+
+        mController.joinAndWait(DEFAULT_DATASET);
+        waitFor(
+                () -> getIpv6Addresses("thread-wpan").contains(mOtCtl.getMlEid()),
+                LINK_ADDR_TIMEOUT);
+
+        IpPrefix meshLocalPrefix = DEFAULT_DATASET.getMeshLocalPrefix();
+        var linkAddrs = FluentIterable.from(getIpv6LinkAddresses("thread-wpan"));
+        var meshLocalAddrs = linkAddrs.filter(addr -> meshLocalPrefix.contains(addr.getAddress()));
+        var mlEidAddrs = meshLocalAddrs.filter(addr -> addr.getAddress().equals(mOtCtl.getMlEid()));
+        var nonMlEidAddrs = meshLocalAddrs.filter(addr -> !mlEidAddrs.contains(addr));
+        assertThat(mlEidAddrs).hasSize(1);
+        assertThat(mlEidAddrs.allMatch(addr -> addr.isPreferred())).isTrue();
+        assertThat(mlEidAddrs.allMatch(addr -> addr.getDeprecationTime() > elapsedRealtime()))
+                .isTrue();
+        assertThat(nonMlEidAddrs).isNotEmpty();
+        assertThat(nonMlEidAddrs.allMatch(addr -> !addr.isPreferred())).isTrue();
+        assertThat(nonMlEidAddrs.allMatch(addr -> addr.getDeprecationTime() <= elapsedRealtime()))
+                .isTrue();
     }
 
     @Test
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index f41e903..f7b4d19 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -479,6 +479,12 @@
         return addresses
     }
 
+    /** Returns the list of [InetAddress] of the given network. */
+    @JvmStatic
+    fun getIpv6Addresses(interfaceName: String): List<InetAddress> {
+        return getIpv6LinkAddresses(interfaceName).map { it.address }
+    }
+
     /** Return the first discovered service of `serviceType`. */
     @JvmStatic
     @Throws(Exception::class)
diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
index 272685f..d35b94e 100644
--- a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
+++ b/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
@@ -24,9 +24,11 @@
 import com.android.compatibility.common.util.SystemUtil;
 
 import java.net.Inet6Address;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * Wrapper of the "/system/bin/ot-ctl" which can be used to send CLI commands to ot-daemon to
@@ -72,6 +74,25 @@
                 .toList();
     }
 
+    /** Returns the OMR address of this device or {@code null} if it doesn't exist. */
+    @Nullable
+    public Inet6Address getOmrAddress() {
+        List<Inet6Address> allAddresses = new ArrayList<>(getAddresses());
+        allAddresses.removeAll(getMeshLocalAddresses());
+
+        List<Inet6Address> omrAddresses =
+                allAddresses.stream()
+                        .filter(addr -> !addr.isLinkLocalAddress())
+                        .collect(Collectors.toList());
+        if (omrAddresses.isEmpty()) {
+            return null;
+        } else if (omrAddresses.size() > 1) {
+            throw new IllegalStateException();
+        }
+
+        return omrAddresses.getFirst();
+    }
+
     /** Returns {@code true} if the Thread interface is up. */
     public boolean isInterfaceUp() {
         String output = executeCommand("ifconfig");