diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java index 0a66f01c7e..2f9c3bce50 100644 --- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java +++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java @@ -37,6 +37,7 @@ import android.os.Parcelable; 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; @@ -1258,6 +1259,44 @@ public class TetheringManager { 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 */ @@ -1279,6 +1318,8 @@ public class TetheringManager { 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 d0ba43174c..70a3442c5a 100644 --- a/Tethering/src/android/net/ip/IpServer.java +++ b/Tethering/src/android/net/ip/IpServer.java @@ -415,12 +415,6 @@ public class IpServer extends StateMachineShim { 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 3ebe4f772a..37ad86a45c 100644 --- a/Tethering/src/com/android/networkstack/tethering/RequestTracker.java +++ b/Tethering/src/com/android/networkstack/tethering/RequestTracker.java @@ -19,6 +19,8 @@ package com.android.networkstack.tethering; 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; @@ -28,6 +30,7 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * Helper class to keep track of tethering requests. @@ -36,29 +39,17 @@ import java.util.List; * 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)}. + * 3) Remove pending request with {@link #removePendingRequest(TetheringRequest)}. * 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; + @NonNull + private final boolean mUseFuzzyMatching; - private PendingRequest(@NonNull TetheringRequest tetheringRequest) { - mTetheringRequest = tetheringRequest; - } - - @NonNull - TetheringRequest getTetheringRequest() { - return mTetheringRequest; - } + public RequestTracker(boolean useFuzzyMatching) { + mUseFuzzyMatching = useFuzzyMatching; } public enum AddResult { @@ -69,24 +60,51 @@ public class RequestTracker { /** * 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. + * pending request before trying to add the result again. Only returned on V-. */ - FAILURE_CONFLICTING_PENDING_REQUEST + 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, } /** - * 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 mPendingRequests = new ArrayList<>(); + private final List mPendingRequests = new ArrayList<>(); + /** + * List of serving requests added by + * {@link #promoteRequestToServing(IpServer, TetheringRequest)}. + */ + private final Map mServingRequests = new ArrayMap<>(); @VisibleForTesting List getPendingTetheringRequests() { - List requests = new ArrayList<>(); - for (PendingRequest pendingRequest : mPendingRequests) { - requests.add(pendingRequest.getTetheringRequest()); + 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 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; + } } - return requests; + + mPendingRequests.add(newRequest); + return AddResult.SUCCESS; } /** @@ -95,9 +113,12 @@ public class RequestTracker { * 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 (PendingRequest request : mPendingRequests) { - TetheringRequest existingRequest = request.getTetheringRequest(); + for (TetheringRequest existingRequest : mPendingRequests) { if (existingRequest.getTetheringType() != newRequest.getTetheringType()) { continue; } @@ -105,7 +126,7 @@ public class RequestTracker { // 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; + return AddResult.FAILURE_DUPLICATE_REQUEST_RESTART; } } @@ -113,7 +134,7 @@ public class RequestTracker { // conflicting parameters above, so these would have been equivalent anyway (except for // UID). removeAllPendingRequests(newRequest.getTetheringType()); - mPendingRequests.add(new PendingRequest(newRequest)); + mPendingRequests.add(newRequest); return AddResult.SUCCESS; } @@ -145,10 +166,8 @@ public class RequestTracker { */ @Nullable public TetheringRequest getNextPendingRequest(int type) { - for (PendingRequest pendingRequest : mPendingRequests) { - TetheringRequest tetheringRequest = - pendingRequest.getTetheringRequest(); - if (tetheringRequest.getTetheringType() == type) return tetheringRequest; + for (TetheringRequest request : mPendingRequests) { + if (request.getTetheringType() == type) return request; } return null; } @@ -158,7 +177,66 @@ public class RequestTracker { * * @param type Tethering type */ - public void removeAllPendingRequests(int type) { - mPendingRequests.removeIf(r -> r.getTetheringRequest().getTetheringType() == 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); } } diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java index 1a26658c7a..7b67095697 100644 --- a/Tethering/src/com/android/networkstack/tethering/Tethering.java +++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java @@ -43,6 +43,7 @@ import static android.net.TetheringManager.TETHERING_WIFI; 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; @@ -67,6 +68,8 @@ import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED; 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; @@ -309,7 +312,7 @@ public class Tethering { mLooper = mDeps.makeTetheringLooper(); mNotificationUpdater = mDeps.makeNotificationUpdater(mContext, mLooper); mTetheringMetrics = mDeps.makeTetheringMetrics(mContext); - mRequestTracker = new RequestTracker(); + mRequestTracker = new RequestTracker(isTetheringWithSoftApConfigEnabled()); mTetherStates = new ArrayMap<>(); mConnectedClientsTracker = new ConnectedClientsTracker(); @@ -460,6 +463,7 @@ public class Tethering { return mSettingsObserver; } + // TODO: Replace with SdkLevel.isAtLeastB() once the feature is fully implemented. boolean isTetheringWithSoftApConfigEnabled() { return SdkLevel.isAtLeastB() && Flags.tetheringWithSoftApConfig(); } @@ -704,9 +708,16 @@ public class Tethering { 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) { + if (result == FAILURE_DUPLICATE_REQUEST_RESTART) { 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()) { @@ -731,7 +742,8 @@ public class Tethering { 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. + // that has not happened yet. This already works if the sync state machine is enabled + // (which includes all B+ devices). if (state.isCurrentlyServing() && state.ipServer.interfaceType() == type) return true; } return false; @@ -742,23 +754,24 @@ public class Tethering { if (!isTetheringWithSoftApConfigEnabled()) return; mHandler.post(() -> { final int type = request.getTetheringType(); + // TODO: when fuzzy-matching is enabled, check whether any pending request + // fuzzy-matches this request instead of just checking the type. if (isTetheringTypePendingOrServing(type)) { stopTetheringInternal(type); - try { - listener.onResult(TETHER_ERROR_NO_ERROR); - } catch (RemoteException ignored) { } + // TODO: We should send the success result after the waiting for tethering to + // actually stop. + sendTetherResult(listener, TETHER_ERROR_NO_ERROR); return; } // Request doesn't match any active requests, ignore. - try { - listener.onResult(TETHER_ERROR_UNKNOWN_REQUEST); - } catch (RemoteException ignored) { } + sendTetherResult(listener, TETHER_ERROR_UNKNOWN_REQUEST); }); } 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,21 +815,25 @@ public class Tethering { // The result of Bluetooth tethering will be sent after the pan service connects. if (result == TETHER_ERROR_BLUETOOTH_SERVICE_PENDING) return; - sendTetherResult(listener, result, type); + sendTetherResultAndRemoveOnError(request, listener, result); } - private void sendTetherResult(final IIntResultListener listener, final int result, - final int type) { + private void sendTetherResult(final IIntResultListener listener, final int result) { 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.removeAllPendingRequests(type); + mRequestTracker.removePendingRequest(request); + final int type = request.getTetheringType(); mTetheringMetrics.updateErrorCode(type, result); mTetheringMetrics.sendReport(type); } @@ -862,8 +879,7 @@ public class Tethering { // 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, - TETHERING_BLUETOOTH); + sendTetherResult(mPendingPanRequestListener, TETHER_ERROR_SERVICE_UNAVAIL); } mPendingPanRequestListener = null; return TETHER_ERROR_NO_ERROR; @@ -904,7 +920,10 @@ public class Tethering { if (mPendingPanRequestListener != null) { final int result = setBluetoothTetheringSettings(mBluetoothPan, true /* enable */); - sendTetherResult(mPendingPanRequestListener, result, TETHERING_BLUETOOTH); + sendTetherResultAndRemoveOnError( + mRequestTracker.getOrCreatePendingRequest(TETHERING_BLUETOOTH), + mPendingPanRequestListener, + result); } mPendingPanRequestListener = null; }); @@ -918,8 +937,10 @@ public class Tethering { mIsConnected = false; if (mPendingPanRequestListener != null) { - sendTetherResult(mPendingPanRequestListener, TETHER_ERROR_SERVICE_UNAVAIL, - TETHERING_BLUETOOTH); + sendTetherResultAndRemoveOnError( + mRequestTracker.getOrCreatePendingRequest(TETHERING_BLUETOOTH), + mPendingPanRequestListener, + TETHER_ERROR_SERVICE_UNAVAIL); } mPendingPanRequestListener = null; mBluetoothIfaceRequest = null; @@ -1093,9 +1114,7 @@ public class Tethering { final int type = ifaceNameToType(iface); if (type == TETHERING_INVALID) { Log.e(TAG, "Ignoring call to legacy tether for unknown iface " + iface); - try { - listener.onResult(TETHER_ERROR_UNKNOWN_IFACE); - } catch (RemoteException e) { } + sendTetherResult(listener, TETHER_ERROR_UNKNOWN_IFACE); } TetheringRequest request = mRequestTracker.getNextPendingRequest(type); @@ -1132,9 +1151,7 @@ public class Tethering { // Do nothing break; } - try { - listener.onResult(result); - } catch (RemoteException e) { } + sendTetherResult(listener, result); } /** @@ -1167,11 +1184,9 @@ public class Tethering { 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, @@ -1191,10 +1206,7 @@ public class Tethering { return; } mHandler.post(() -> { - try { - listener.onResult(legacyUntetherInternal(iface)); - } catch (RemoteException e) { - } + sendTetherResult(listener, legacyUntetherInternal(iface)); }); } @@ -1209,7 +1221,7 @@ public class Tethering { Log.e(TAG, "Tried to untether an inactive iface :" + iface + ", ignoring"); return TETHER_ERROR_UNAVAIL_IFACE; } - tetherState.ipServer.unwanted(); + ensureIpServerUnwanted(tetherState.ipServer); return TETHER_ERROR_NO_ERROR; } @@ -1289,7 +1301,12 @@ public class Tethering { final TetherState tetherState = mTetherStates.valueAt(i); final int type = tetherState.ipServer.interfaceType(); final String iface = mTetherStates.keyAt(i); - final TetheringRequest request = tetherState.ipServer.getTetheringRequest(); + // 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 boolean includeSoftApConfig = request != null && cookie != null && (cookie.uid == request.getUid() || cookie.hasSystemPrivilege); final TetheringInterface tetheringIface = new TetheringInterface(type, iface, @@ -1611,7 +1628,7 @@ public class Tethering { private void disableWifiIpServingCommon(int tetheringType, String ifname) { if (!TextUtils.isEmpty(ifname) && mTetherStates.containsKey(ifname)) { - mTetherStates.get(ifname).ipServer.unwanted(); + ensureIpServerUnwanted(mTetherStates.get(ifname).ipServer); return; } @@ -1628,7 +1645,7 @@ public class Tethering { for (int i = 0; i < mTetherStates.size(); i++) { final IpServer ipServer = mTetherStates.valueAt(i).ipServer; if (ipServer.interfaceType() == tetheringType) { - ipServer.unwanted(); + ensureIpServerUnwanted(ipServer); return; } } @@ -1775,9 +1792,7 @@ public class Tethering { void setUsbTethering(boolean enable, IIntResultListener listener) { mHandler.post(() -> { - try { - listener.onResult(setUsbTethering(enable)); - } catch (RemoteException e) { } + sendTetherResult(listener, setUsbTethering(enable)); }); } @@ -2904,6 +2919,9 @@ public class Tethering { 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); } @@ -2918,7 +2936,19 @@ public class Tethering { 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: @@ -3014,11 +3044,18 @@ public class Tethering { 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); } @@ -3026,9 +3063,7 @@ public class Tethering { void setPreferTestNetworks(final boolean prefer, IIntResultListener listener) { mHandler.post(() -> { mUpstreamNetworkMonitor.setPreferTestNetworks(prefer); - try { - listener.onResult(TETHER_ERROR_NO_ERROR); - } catch (RemoteException e) { } + sendTetherResult(listener, TETHER_ERROR_NO_ERROR); }); } 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 e00e9f001f..086f2d2070 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/RequestTrackerTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/RequestTrackerTest.java @@ -18,19 +18,22 @@ package com.android.networkstack.tethering; 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; @@ -39,13 +42,10 @@ import org.junit.runner.RunWith; 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,6 +53,7 @@ public class RequestTrackerTest { @Test public void testAddRequest_successResultAndBecomesNextPending() { + mRequestTracker = new RequestTracker(false); final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build(); final AddResult result = mRequestTracker.addPendingRequest(request); @@ -64,6 +65,7 @@ public class RequestTrackerTest { @Test public void testAddRequest_equalRequestExists_successResultAndBecomesNextPending() { + mRequestTracker = new RequestTracker(false); final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build(); mRequestTracker.addPendingRequest(request); @@ -77,6 +79,7 @@ public class RequestTrackerTest { @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"); @@ -94,7 +97,8 @@ public class RequestTrackerTest { } @Test - public void testAddConflictingRequest_returnsFailureConflictingPendingRequest() { + public void testAddRequest_conflictingPendingRequest_returnsFailureConflictingRequestRestart() { + mRequestTracker = new RequestTracker(false); final TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build(); final TetheringRequest conflictingRequest = new TetheringRequest.Builder(TETHERING_WIFI) .setExemptFromEntitlementCheck(true).build(); @@ -102,13 +106,139 @@ public class RequestTrackerTest { final AddResult result = mRequestTracker.addPendingRequest(conflictingRequest); - assertThat(result).isEqualTo(AddResult.FAILURE_CONFLICTING_PENDING_REQUEST); + assertThat(result).isEqualTo(AddResult.FAILURE_DUPLICATE_REQUEST_RESTART); 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"); @@ -127,6 +257,7 @@ public class RequestTrackerTest { @Test public void testRemoveAllPendingRequests_differentTypeExists_doesNotRemoveDifferentType() { + mRequestTracker = new RequestTracker(false); final TetheringRequest differentType = new TetheringRequest.Builder(TETHERING_USB).build(); mRequestTracker.addPendingRequest(differentType); @@ -136,4 +267,61 @@ public class RequestTrackerTest { 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 74bab03bb1..df8e1b759e 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java @@ -50,6 +50,7 @@ import static android.net.TetheringManager.TETHERING_USB; 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; @@ -59,6 +60,7 @@ import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_FAILED; import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STARTED; import static android.net.TetheringManager.TETHER_HARDWARE_OFFLOAD_STOPPED; import static android.net.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; @@ -93,6 +95,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.notNull; @@ -197,6 +200,7 @@ import androidx.test.runner.AndroidJUnit4; 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; @@ -308,8 +312,7 @@ public class TetheringTest { @Mock private TetheringMetrics mTetheringMetrics; @Mock private PrivateAddressCoordinator.Dependencies mPrivateAddressCoordinatorDependencies; - private final MockIpServerDependencies mIpServerDependencies = - spy(new MockIpServerDependencies()); + private MockIpServerDependencies mIpServerDependencies; private final MockTetheringDependencies mTetheringDependencies = new MockTetheringDependencies(); @@ -396,6 +399,9 @@ public class TetheringTest { } public class MockIpServerDependencies extends IpServer.Dependencies { + + private int mOnDhcpServerCreatedResult = STATUS_SUCCESS; + @Override public DadProxy getDadProxy( Handler handler, InterfaceParams ifParams) { @@ -437,7 +443,7 @@ public class TetheringTest { DhcpServerCallbacks cb) { new Thread(() -> { try { - cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer); + cb.onDhcpServerCreated(mOnDhcpServerCreatedResult, mDhcpServer); } catch (RemoteException e) { fail(e.getMessage()); } @@ -448,6 +454,10 @@ public class TetheringTest { IpNeighborMonitor.NeighborEventConsumer c) { return mIpNeighborMonitor; } + + public void setOnDhcpServerCreatedResult(final int result) { + mOnDhcpServerCreatedResult = result; + } } public class MockTetheringDependencies extends TetheringDependencies { @@ -708,6 +718,7 @@ public class TetheringTest { 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. @@ -2425,7 +2436,7 @@ public class TetheringTest { @Test public void testSoftApConfigInTetheringEventCallback() throws Exception { - assumeTrue(SdkLevel.isAtLeastV()); + assumeTrue(isTetheringWithSoftApConfigEnabled()); when(mContext.checkCallingOrSelfPermission(NETWORK_SETTINGS)) .thenReturn(PERMISSION_DENIED); when(mContext.checkCallingOrSelfPermission(NETWORK_STACK)) @@ -2482,14 +2493,12 @@ public class TetheringTest { // Enable wifi tethering mBinderCallingUid = TEST_CALLER_UID; mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, null); + mLooper.dispatchAll(); mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); mLooper.dispatchAll(); - if (SdkLevel.isAtLeastB()) { - // Starting in B, ignore the interfaceStatusChanged - callback.assertNoStateChangeCallback(); - } + // Netd "up" event should not trigger a state change callback in B+. + 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); @@ -2507,20 +2516,15 @@ public class TetheringTest { callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STARTED); // Disable wifi tethering - mLooper.dispatchAll(); 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, + mLooper.dispatchAll(); + sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED); - } - assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig}, + assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig}, callback.pollTetherStatesChanged().availableList); assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig}, differentCallback.pollTetherStatesChanged().availableList); - assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig}, + assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig}, settingsCallback.pollTetherStatesChanged().availableList); mLooper.dispatchAll(); callback.expectUpstreamChanged(NULL_NETWORK); @@ -2528,6 +2532,253 @@ public class TetheringTest { 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 testReportFailCallbackIfOffloadNotSupported() throws Exception { initTetheringOnTestThread(); @@ -2794,7 +3045,8 @@ public class TetheringTest { } @Test - public void testMultipleStartTethering() throws Exception { + public void testMultipleStartTetheringLegacy() throws Exception { + assumeFalse(isTetheringWithSoftApConfigEnabled()); initTetheringOnTestThread(); final LinkAddress serverLinkAddr = new LinkAddress("192.168.20.1/24"); final LinkAddress clientLinkAddr = new LinkAddress("192.168.20.42/24"); @@ -3575,6 +3827,7 @@ public class TetheringTest { @Test public void testStartBluetoothTetheringFailsWhenTheresAnExistingRequestWaitingForPanService() throws Exception { + assumeFalse(isTetheringWithSoftApConfigEnabled()); initTetheringOnTestThread(); mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */); diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java index e645f671ae..52799abbb7 100644 --- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java +++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java @@ -32,6 +32,7 @@ import static android.net.TetheringManager.TETHERING_USB; 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; @@ -234,6 +235,30 @@ public class TetheringManagerTest { } + @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()) { @@ -380,17 +405,20 @@ public class TetheringManagerTest { tetherEventCallback.assumeWifiTetheringSupported(mContext); tetherEventCallback.expectNoTetheringActive(); - SoftApConfiguration softApConfig = createSoftApConfiguration("SSID"); + SoftApConfiguration softApConfig = isTetheringWithSoftApConfigEnabled() + ? createSoftApConfiguration("SSID") : null; final TetheringInterface tetheredIface = mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig); assertNotNull(tetheredIface); - assertEquals(softApConfig, tetheredIface.getSoftApConfiguration()); - final String wifiTetheringIface = tetheredIface.getInterface(); + if (isTetheringWithSoftApConfigEnabled()) { + assertEquals(softApConfig, tetheredIface.getSoftApConfiguration()); + } mCtsTetheringUtils.stopWifiTethering(tetherEventCallback); if (!SdkLevel.isAtLeastB()) { + final String wifiTetheringIface = tetheredIface.getInterface(); try { final int ret = runAsShell(TETHER_PRIVILEGED, () -> mTM.tether(wifiTetheringIface));