Merge "no-op: Use helper class to handle pending requests" into main
diff --git a/Tethering/src/com/android/networkstack/tethering/RequestTracker.java b/Tethering/src/com/android/networkstack/tethering/RequestTracker.java
new file mode 100644
index 0000000..3ebe4f7
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/RequestTracker.java
@@ -0,0 +1,164 @@
+/*
+ * 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.networkstack.tethering;
+
+import static com.android.networkstack.tethering.util.TetheringUtils.createPlaceholderRequest;
+
+import android.net.TetheringManager.TetheringRequest;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to keep track of tethering requests.
+ * The intended usage of this class is
+ * 1) Add a pending request with {@link #addPendingRequest(TetheringRequest)} before asking the link
+ *    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 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();
+
+    private class PendingRequest {
+        @NonNull
+        private final TetheringRequest mTetheringRequest;
+
+        private PendingRequest(@NonNull TetheringRequest tetheringRequest) {
+            mTetheringRequest = tetheringRequest;
+        }
+
+        @NonNull
+        TetheringRequest getTetheringRequest() {
+            return mTetheringRequest;
+        }
+    }
+
+    public enum AddResult {
+        /**
+         * Request was successfully added
+         */
+        SUCCESS,
+        /**
+         * 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.
+         */
+        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.
+     */
+    private final List<PendingRequest> mPendingRequests = new ArrayList<>();
+
+    @VisibleForTesting
+    List<TetheringRequest> getPendingTetheringRequests() {
+        List<TetheringRequest> requests = new ArrayList<>();
+        for (PendingRequest pendingRequest : mPendingRequests) {
+            requests.add(pendingRequest.getTetheringRequest());
+        }
+        return requests;
+    }
+
+    /**
+     * Add a pending request and listener. The request should be added before asking the link layer
+     * to start, and should be retrieved with {@link #getNextPendingRequest(int)} once the link
+     * layer comes up. The result of the add operation will be returned as an AddResult code.
+     */
+    public AddResult addPendingRequest(@NonNull final TetheringRequest newRequest) {
+        // Check the existing requests to see if it is OK to add the new request.
+        for (PendingRequest request : mPendingRequests) {
+            TetheringRequest existingRequest = request.getTetheringRequest();
+            if (existingRequest.getTetheringType() != newRequest.getTetheringType()) {
+                continue;
+            }
+
+            // Can't add request if there's a request of the same type with different
+            // parameters.
+            if (!existingRequest.equalsIgnoreUidPackage(newRequest)) {
+                return AddResult.FAILURE_CONFLICTING_PENDING_REQUEST;
+            }
+        }
+
+        // Remove the existing pending request of the same type. We already filter out for
+        // conflicting parameters above, so these would have been equivalent anyway (except for
+        // UID).
+        removeAllPendingRequests(newRequest.getTetheringType());
+        mPendingRequests.add(new PendingRequest(newRequest));
+        return AddResult.SUCCESS;
+    }
+
+    /**
+     * Gets the next pending TetheringRequest of a given type, or creates a placeholder request if
+     * there are none.
+     * Note: There are edge cases where the pending request is absent and we must temporarily
+     * synthesize a placeholder request, such as if stopTethering was called before link
+     * layer went up, or if the link layer goes up without us poking it (e.g. adb shell
+     * cmd wifi start-softap). These placeholder requests only specify the tethering type
+     * and the default connectivity scope.
+     */
+    @NonNull
+    public TetheringRequest getOrCreatePendingRequest(int type) {
+        TetheringRequest pending = getNextPendingRequest(type);
+        if (pending != null) return pending;
+
+        Log.w(TAG, "No pending TetheringRequest for type " + type + " found, creating a"
+                + " placeholder request");
+        return createPlaceholderRequest(type);
+    }
+
+    /**
+     * Same as {@link #getOrCreatePendingRequest(int)} but returns {@code null} if there's no
+     * pending request found.
+     *
+     * @param type Tethering type of the pending request
+     * @return pending request or {@code null} if there are none.
+     */
+    @Nullable
+    public TetheringRequest getNextPendingRequest(int type) {
+        for (PendingRequest pendingRequest : mPendingRequests) {
+            TetheringRequest tetheringRequest =
+                    pendingRequest.getTetheringRequest();
+            if (tetheringRequest.getTetheringType() == type) return tetheringRequest;
+        }
+        return null;
+    }
+
+    /**
+     * Removes all pending requests of the given tethering type.
+     *
+     * @param type Tethering type
+     */
+    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 e37c5db..1589509 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -29,7 +29,6 @@
 import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
-import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY;
 import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER;
 import static android.net.TetheringManager.EXTRA_AVAILABLE_TETHER;
@@ -78,6 +77,9 @@
 import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_LEGACY_TETHER_WITH_TYPE_WIFI_SUCCESS;
 import static com.android.networkstack.tethering.metrics.TetheringStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_TETHER_WITH_PLACEHOLDER_REQUEST;
 import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_MAIN_SM;
+import static com.android.networkstack.tethering.util.TetheringUtils.createImplicitLocalOnlyTetheringRequest;
+import static com.android.networkstack.tethering.util.TetheringUtils.createLegacyGlobalScopeTetheringRequest;
+import static com.android.networkstack.tethering.util.TetheringUtils.createPlaceholderRequest;
 
 import android.app.usage.NetworkStatsManager;
 import android.bluetooth.BluetoothAdapter;
@@ -241,11 +243,7 @@
     private final SharedLog mLog = new SharedLog(TAG);
     private final RemoteCallbackList<ITetheringEventCallback> mTetheringEventCallbacks =
             new RemoteCallbackList<>();
-    // Currently active tethering requests per tethering type. Only one of each type can be
-    // requested at a time. After a tethering type is requested, the map keeps tethering parameters
-    // to be used after the interface comes up asynchronously.
-    private final SparseArray<TetheringRequest> mPendingTetheringRequests =
-            new SparseArray<>();
+    private final RequestTracker mRequestTracker;
 
     private final Context mContext;
     private final ArrayMap<String, TetherState> mTetherStates;
@@ -308,6 +306,7 @@
         mLooper = mDeps.makeTetheringLooper();
         mNotificationUpdater = mDeps.makeNotificationUpdater(mContext, mLooper);
         mTetheringMetrics = mDeps.makeTetheringMetrics(mContext);
+        mRequestTracker = new RequestTracker();
 
         // This is intended to ensrure that if something calls startTethering(bluetooth) just after
         // bluetooth is enabled. Before onServiceConnected is called, store the calls into this
@@ -704,13 +703,13 @@
             final IIntResultListener listener) {
         mHandler.post(() -> {
             final int type = request.getTetheringType();
-            final TetheringRequest unfinishedRequest = mPendingTetheringRequests.get(type);
-            // If tethering is already enabled with a different request,
-            // disable before re-enabling.
-            if (unfinishedRequest != null && !unfinishedRequest.equalsIgnoreUidPackage(request)) {
-                stopTetheringInternal(type);
+            RequestTracker.AddResult result = mRequestTracker.addPendingRequest(request);
+            // If tethering is already pending with a conflicting request, stop tethering before
+            // starting.
+            if (result == RequestTracker.AddResult.FAILURE_CONFLICTING_PENDING_REQUEST) {
+                stopTetheringInternal(type); // Also removes the request from the tracker.
+                mRequestTracker.addPendingRequest(request);
             }
-            mPendingTetheringRequests.put(type, request);
 
             if (request.isExemptFromEntitlementCheck()) {
                 mEntitlementMgr.setExemptedDownstreamType(type);
@@ -730,9 +729,7 @@
     }
 
     private boolean isTetheringTypePendingOrServing(final int type) {
-        for (int i = 0; i < mPendingTetheringRequests.size(); i++) {
-            if (mPendingTetheringRequests.valueAt(i).getTetheringType() == type) return true;
-        }
+        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
@@ -763,7 +760,7 @@
     }
 
     void stopTetheringInternal(int type) {
-        mPendingTetheringRequests.remove(type);
+        mRequestTracker.removeAllPendingRequests(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.
@@ -821,7 +818,7 @@
         // If changing tethering fail, remove corresponding request
         // no matter who trigger the start/stop.
         if (result != TETHER_ERROR_NO_ERROR) {
-            mPendingTetheringRequests.remove(type);
+            mRequestTracker.removeAllPendingRequests(type);
             mTetheringMetrics.updateErrorCode(type, result);
             mTetheringMetrics.sendReport(type);
         }
@@ -990,7 +987,7 @@
             if (this != mBluetoothCallback) return;
 
             final TetheringRequest request =
-                    getOrCreatePendingTetheringRequest(TETHERING_BLUETOOTH);
+                    mRequestTracker.getOrCreatePendingRequest(TETHERING_BLUETOOTH);
             enableIpServing(request, iface);
             mConfiguredBluetoothIface = iface;
         }
@@ -1047,7 +1044,8 @@
                 return;
             }
 
-            final TetheringRequest request = getOrCreatePendingTetheringRequest(TETHERING_ETHERNET);
+            final TetheringRequest request = mRequestTracker.getOrCreatePendingRequest(
+                    TETHERING_ETHERNET);
             enableIpServing(request, iface);
             mConfiguredEthernetIface = iface;
         }
@@ -1079,61 +1077,6 @@
         return TETHER_ERROR_NO_ERROR;
     }
 
-    /**
-     * Create a legacy tethering request for calls to the legacy tether() API, which doesn't take an
-     * explicit request. These are always CONNECTIVITY_SCOPE_GLOBAL, per historical behavior.
-     */
-    private TetheringRequest createLegacyGlobalScopeTetheringRequest(int type) {
-        final TetheringRequest request = new TetheringRequest.Builder(type).build();
-        request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_LEGACY;
-        request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
-        return request;
-    }
-
-    /**
-     * Create a local-only implicit tethering request. This is used for Wifi local-only hotspot and
-     * Wifi P2P, which start tethering based on the WIFI_(AP/P2P)_STATE_CHANGED broadcasts.
-     */
-    @NonNull
-    private TetheringRequest createImplicitLocalOnlyTetheringRequest(int type) {
-        final TetheringRequest request = new TetheringRequest.Builder(type).build();
-        request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_IMPLICIT;
-        request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_LOCAL;
-        return request;
-    }
-
-    /**
-     * Create a placeholder request. This is used in case we try to find a pending request but there
-     * is none (e.g. stopTethering removed a pending request), or for cases where we only have the
-     * tethering type (e.g. stopTethering(int)).
-     */
-    @NonNull
-    private TetheringRequest createPlaceholderRequest(int type) {
-        final TetheringRequest request = new TetheringRequest.Builder(type).build();
-        request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_LEGACY;
-        request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
-        return request;
-    }
-
-    /**
-     * Gets the TetheringRequest that #startTethering was called with but is waiting for the link
-     * layer event to indicate the interface is available to tether.
-     * Note: There are edge cases where the pending request is absent and we must temporarily
-     *       synthesize a placeholder request, such as if stopTethering was called before link layer
-     *       went up, or if the link layer goes up without us poking it (e.g. adb shell cmd wifi
-     *       start-softap). These placeholder requests only specify the tethering type and the
-     *       default connectivity scope.
-     */
-    @NonNull
-    private TetheringRequest getOrCreatePendingTetheringRequest(int type) {
-        TetheringRequest pending = mPendingTetheringRequests.get(type);
-        if (pending != null) return pending;
-
-        Log.w(TAG, "No pending TetheringRequest for type " + type + " found, creating a placeholder"
-                + " request");
-        return createPlaceholderRequest(type);
-    }
-
     private void handleLegacyTether(String iface, final IIntResultListener listener) {
         if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
             // After V, the TetheringManager and ConnectivityManager tether and untether methods
@@ -1151,7 +1094,7 @@
             } catch (RemoteException e) { }
         }
 
-        TetheringRequest request = getPendingTetheringRequest(type);
+        TetheringRequest request = mRequestTracker.getNextPendingRequest(type);
         if (request == null) {
             request = createLegacyGlobalScopeTetheringRequest(type);
         }
@@ -1224,7 +1167,7 @@
         // 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.
-        mPendingTetheringRequests.remove(request.getTetheringType());
+        mRequestTracker.removeAllPendingRequests(request.getTetheringType());
         tetherState.ipServer.enable(request);
         if (request.getRequestType() == REQUEST_TYPE_PLACEHOLDER) {
             TerribleErrorLog.logTerribleError(TetheringStatsLog::write,
@@ -1590,8 +1533,8 @@
     }
 
     @VisibleForTesting
-    SparseArray<TetheringRequest> getPendingTetheringRequests() {
-        return mPendingTetheringRequests;
+    List<TetheringRequest> getPendingTetheringRequests() {
+        return mRequestTracker.getPendingTetheringRequests();
     }
 
     @VisibleForTesting
@@ -1651,10 +1594,6 @@
         }
     }
 
-    final TetheringRequest getPendingTetheringRequest(int type) {
-        return mPendingTetheringRequests.get(type, null);
-    }
-
     private void enableIpServing(@NonNull TetheringRequest request, String ifname) {
         enableIpServing(request, ifname, false /* isNcm */);
     }
@@ -1742,7 +1681,7 @@
         switch (wifiIpMode) {
             case IFACE_IP_MODE_TETHERED:
                 type = maybeInferWifiTetheringType(ifname);
-                request = getOrCreatePendingTetheringRequest(type);
+                request = mRequestTracker.getOrCreatePendingRequest(type);
                 // Wifi requests will always have CONNECTIVITY_SCOPE_GLOBAL, because
                 // TetheringRequest.Builder will not allow callers to set CONNECTIVITY_SCOPE_LOCAL
                 // for TETHERING_WIFI. However, if maybeInferWifiTetheringType returns a non-Wifi
@@ -1797,7 +1736,7 @@
             return;
         }
 
-        final TetheringRequest request = getOrCreatePendingTetheringRequest(tetheringType);
+        final TetheringRequest request = mRequestTracker.getOrCreatePendingRequest(tetheringType);
         if (ifaces != null) {
             for (String iface : ifaces) {
                 if (ifaceNameToType(iface) == tetheringType) {
diff --git a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
index 76c2f0d..79e6e16 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
@@ -15,7 +15,11 @@
  */
 package com.android.networkstack.tethering.util;
 
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
+
 import android.net.TetherStatsParcel;
+import android.net.TetheringManager.TetheringRequest;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -166,4 +170,41 @@
             return null;
         }
     }
+
+    /**
+     * Create a legacy tethering request for calls to the legacy tether() API, which doesn't take an
+     * explicit request. These are always CONNECTIVITY_SCOPE_GLOBAL, per historical behavior.
+     */
+    @NonNull
+    public static TetheringRequest createLegacyGlobalScopeTetheringRequest(int type) {
+        final TetheringRequest request = new TetheringRequest.Builder(type).build();
+        request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_LEGACY;
+        request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
+        return request;
+    }
+
+    /**
+     * Create a local-only implicit tethering request. This is used for Wifi local-only hotspot and
+     * Wifi P2P, which start tethering based on the WIFI_(AP/P2P)_STATE_CHANGED broadcasts.
+     */
+    @NonNull
+    public static TetheringRequest createImplicitLocalOnlyTetheringRequest(int type) {
+        final TetheringRequest request = new TetheringRequest.Builder(type).build();
+        request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_IMPLICIT;
+        request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_LOCAL;
+        return request;
+    }
+
+    /**
+     * Create a placeholder request. This is used in case we try to find a pending request but there
+     * is none (e.g. stopTethering removed a pending request), or for cases where we only have the
+     * tethering type (e.g. stopTethering(int)).
+     */
+    @NonNull
+    public static TetheringRequest createPlaceholderRequest(int type) {
+        final TetheringRequest request = new TetheringRequest.Builder(type).build();
+        request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_PLACEHOLDER;
+        request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
+        return request;
+    }
 }
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index d0d23ac..ee82776 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -57,6 +57,7 @@
         "mockito-target-extended-minus-junit4",
         "net-tests-utils",
         "testables",
+        "truth",
     ],
     // TODO(b/147200698) change sdk_version to module-current and
     // remove framework-minus-apex, ext, and framework-res
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/RequestTrackerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/RequestTrackerTest.java
new file mode 100644
index 0000000..e00e9f0
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/RequestTrackerTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.networkstack.tethering;
+
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+
+import static com.android.networkstack.tethering.util.TetheringUtils.createPlaceholderRequest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.TetheringManager.TetheringRequest;
+
+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;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RequestTrackerTest {
+    private RequestTracker mRequestTracker;
+
+    @Before
+    public void setUp() {
+        mRequestTracker = new RequestTracker();
+    }
+
+    @Test
+    public void testNoRequestsAdded_noPendingRequests() {
+        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isNull();
+        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI))
+                .isEqualTo(createPlaceholderRequest(TETHERING_WIFI));
+    }
+
+    @Test
+    public void testAddRequest_successResultAndBecomesNextPending() {
+        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_equalRequestExists_successResultAndBecomesNextPending() {
+        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+        mRequestTracker.addPendingRequest(request);
+
+        final TetheringRequest equalRequest = new TetheringRequest.Builder(TETHERING_WIFI).build();
+        final AddResult result = mRequestTracker.addPendingRequest(equalRequest);
+
+        assertThat(result).isEqualTo(AddResult.SUCCESS);
+        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isEqualTo(request);
+        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI)).isEqualTo(request);
+    }
+
+    @Test
+    public void testAddRequest_equalButDifferentUidRequest_successResultAndBecomesNextPending() {
+        final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
+        request.setUid(1000);
+        request.setPackageName("package");
+        final TetheringRequest differentUid = new TetheringRequest.Builder(TETHERING_WIFI).build();
+        differentUid.setUid(2000);
+        differentUid.setPackageName("package2");
+        mRequestTracker.addPendingRequest(request);
+
+        final AddResult result = mRequestTracker.addPendingRequest(differentUid);
+
+        assertThat(result).isEqualTo(AddResult.SUCCESS);
+        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isEqualTo(differentUid);
+        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI))
+                .isEqualTo(differentUid);
+    }
+
+    @Test
+    public void testAddConflictingRequest_returnsFailureConflictingPendingRequest() {
+        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_CONFLICTING_PENDING_REQUEST);
+        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isEqualTo(request);
+        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI)).isEqualTo(request);
+    }
+
+    @Test
+    public void testRemoveAllPendingRequests_noPendingRequestsLeft() {
+        final TetheringRequest firstRequest = new TetheringRequest.Builder(TETHERING_WIFI).build();
+        firstRequest.setUid(1000);
+        firstRequest.setPackageName("package");
+        mRequestTracker.addPendingRequest(firstRequest);
+        final TetheringRequest secondRequest = new TetheringRequest.Builder(TETHERING_WIFI).build();
+        secondRequest.setUid(2000);
+        secondRequest.setPackageName("package2");
+        mRequestTracker.addPendingRequest(secondRequest);
+
+        mRequestTracker.removeAllPendingRequests(TETHERING_WIFI);
+
+        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_WIFI)).isNull();
+        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_WIFI))
+                .isEqualTo(createPlaceholderRequest(TETHERING_WIFI));
+    }
+
+    @Test
+    public void testRemoveAllPendingRequests_differentTypeExists_doesNotRemoveDifferentType() {
+        final TetheringRequest differentType = new TetheringRequest.Builder(TETHERING_USB).build();
+        mRequestTracker.addPendingRequest(differentType);
+
+        mRequestTracker.removeAllPendingRequests(TETHERING_WIFI);
+
+        assertThat(mRequestTracker.getNextPendingRequest(TETHERING_USB)).isEqualTo(differentType);
+        assertThat(mRequestTracker.getOrCreatePendingRequest(TETHERING_USB))
+                .isEqualTo(differentType);
+    }
+}
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 50ecfe1..51efaf8 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -964,7 +964,7 @@
         mLooper.dispatchAll();
 
         assertEquals(1, mTethering.getPendingTetheringRequests().size());
-        assertEquals(request, mTethering.getPendingTetheringRequests().get(TETHERING_USB));
+        assertTrue(mTethering.getPendingTetheringRequests().get(0).equals(request));
 
         if (mTethering.getTetheringConfiguration().isUsingNcm()) {
             verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NCM);