Merge changes I65d57e0e,I8ca1f7ae into main
* changes:
[Thread] unregister all country code callbacks when CountryCode is not needed
[Thread] do not set the country code again if the co-processor doesn't support it
diff --git a/Tethering/apex/canned_fs_config b/Tethering/apex/canned_fs_config
index 1f5fcfa..edc5515 100644
--- a/Tethering/apex/canned_fs_config
+++ b/Tethering/apex/canned_fs_config
@@ -1,3 +1,3 @@
-/bin/for-system 0 1000 0750
+/bin/for-system 1029 1000 0750
/bin/for-system/clatd 1029 1029 06755
/bin/netbpfload 0 0 0750
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 0a66f01..a8a471d 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.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;
@@ -339,6 +340,11 @@
* @hide
*/
public static final int TETHER_ERROR_BLUETOOTH_SERVICE_PENDING = 19;
+ /**
+ * Never used outside Tethering.java.
+ * @hide
+ */
+ public static final int TETHER_ERROR_SOFT_AP_CALLBACK_PENDING = 20;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@@ -1258,6 +1264,44 @@
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 +1323,8 @@
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 d0ba431..70a3442 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -415,12 +415,6 @@
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 3ebe4f7..c91ff58 100644
--- a/Tethering/src/com/android/networkstack/tethering/RequestTracker.java
+++ b/Tethering/src/com/android/networkstack/tethering/RequestTracker.java
@@ -19,6 +19,8 @@
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 java.util.ArrayList;
import java.util.List;
+import java.util.Map;
/**
* Helper class to keep track of tethering requests.
@@ -36,29 +39,17 @@
* 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 @@
/**
* 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<PendingRequest> mPendingRequests = new ArrayList<>();
+ 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<>();
@VisibleForTesting
List<TetheringRequest> getPendingTetheringRequests() {
- List<TetheringRequest> 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<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;
+ }
}
- return requests;
+
+ mPendingRequests.add(newRequest);
+ return AddResult.SUCCESS;
}
/**
@@ -95,9 +113,12 @@
* 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 @@
// 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 @@
// 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 @@
*/
@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,88 @@
*
* @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);
+ }
+
+ @VisibleForTesting
+ List<TetheringRequest> getServingTetheringRequests() {
+ return new ArrayList<>(mServingRequests.values());
+ }
+
+ /**
+ * 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;
}
}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 1a26658..0cf008b 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -43,9 +43,11 @@
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;
+import static android.net.TetheringManager.TETHER_ERROR_SOFT_AP_CALLBACK_PENDING;
import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_REQUEST;
@@ -67,6 +69,8 @@
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;
@@ -111,6 +115,7 @@
import android.net.TetheringManager.TetheringRequest;
import android.net.Uri;
import android.net.ip.IpServer;
+import android.net.wifi.SoftApState;
import android.net.wifi.WifiClient;
import android.net.wifi.WifiManager;
import android.net.wifi.p2p.WifiP2pGroup;
@@ -145,7 +150,6 @@
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.modules.utils.build.SdkLevel;
-import com.android.net.flags.Flags;
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.HandlerUtils;
@@ -309,7 +313,7 @@
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();
@@ -461,7 +465,7 @@
}
boolean isTetheringWithSoftApConfigEnabled() {
- return SdkLevel.isAtLeastB() && Flags.tetheringWithSoftApConfig();
+ return mDeps.isTetheringWithSoftApConfigEnabled();
}
/**
@@ -704,9 +708,16 @@
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()) {
@@ -726,39 +737,28 @@
});
}
- 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(() -> {
- final int type = request.getTetheringType();
- if (isTetheringTypePendingOrServing(type)) {
+ if (mRequestTracker.findFuzzyMatchedRequest(request, !hasNetworkSettings) != null) {
+ final int type = request.getTetheringType();
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.
@@ -777,7 +777,7 @@
final int result;
switch (type) {
case TETHERING_WIFI:
- result = setWifiTethering(enable);
+ result = setWifiTethering(enable, request, listener);
break;
case TETHERING_USB:
result = setUsbTethering(enable);
@@ -802,27 +802,35 @@
// 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);
+ // The result of Wifi tethering will be sent after the SoftApCallback result.
+ if (result == TETHER_ERROR_SOFT_AP_CALLBACK_PENDING) return;
+
+ 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) {
+ }
}
+ }
- // If changing tethering fail, remove corresponding request
- // no matter who trigger the start/stop.
+ private void sendTetherResultAndRemoveOnError(TetheringRequest request,
+ final IIntResultListener listener, final int result) {
+ sendTetherResult(listener, result);
+
if (result != TETHER_ERROR_NO_ERROR) {
- mRequestTracker.removeAllPendingRequests(type);
+ mRequestTracker.removePendingRequest(request);
+ final int type = request.getTetheringType();
mTetheringMetrics.updateErrorCode(type, result);
mTetheringMetrics.sendReport(type);
}
}
- private int setWifiTethering(final boolean enable) {
+ private int setWifiTethering(final boolean enable, TetheringRequest request,
+ IIntResultListener listener) {
final long ident = Binder.clearCallingIdentity();
try {
final WifiManager mgr = getWifiManager();
@@ -830,8 +838,34 @@
mLog.e("setWifiTethering: failed to get WifiManager!");
return TETHER_ERROR_SERVICE_UNAVAIL;
}
- if ((enable && mgr.startTetheredHotspot(null /* use existing softap config */))
- || (!enable && mgr.stopSoftAp())) {
+ final boolean success;
+ if (enable) {
+ if (isTetheringWithSoftApConfigEnabled()) {
+ // Notes:
+ // - A call to startTetheredHotspot can only succeed if the SoftAp is idle. If
+ // the SoftAp is running or is being disabled, the call will fail.
+ // - If a call to startTetheredHotspot fails, the callback is immediately called
+ // with WIFI_AP_STATE_FAILED and a null interface.
+ // - If a call to startTetheredHotspot succeeds, the passed-in callback is the
+ // only callback that will receive future WIFI_AP_STATE_ENABLED and
+ // WIFI_AP_STATE_DISABLED events in the future, until another call to
+ // startTetheredHotspot succeeds, at which point the old callback will stop
+ // receiving any events.
+ // - Wifi may decide to restart the hotspot at any time (such as for a CC
+ // change), and if it does so, it will send WIFI_AP_STATE_DISABLED and then
+ // either WIFI_AP_STATE_ENABLED or (if restarting fails) WIFI_AP_STATE_FAILED.
+ mgr.startTetheredHotspot(request, mExecutor,
+ new StartTetheringSoftApCallback(listener));
+ // Result isn't used since we get the real result via
+ // StartTetheringSoftApCallback.
+ return TETHER_ERROR_SOFT_AP_CALLBACK_PENDING;
+ }
+ success = mgr.startTetheredHotspot(null);
+ } else {
+ success = mgr.stopSoftAp();
+ }
+
+ if (success) {
return TETHER_ERROR_NO_ERROR;
}
} finally {
@@ -862,8 +896,7 @@
// 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 +937,10 @@
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 +954,10 @@
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 +1131,7 @@
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 +1168,7 @@
// Do nothing
break;
}
- try {
- listener.onResult(result);
- } catch (RemoteException e) { }
+ sendTetherResult(listener, result);
}
/**
@@ -1167,11 +1201,9 @@
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 +1223,7 @@
return;
}
mHandler.post(() -> {
- try {
- listener.onResult(legacyUntetherInternal(iface));
- } catch (RemoteException e) {
- }
+ sendTetherResult(listener, legacyUntetherInternal(iface));
});
}
@@ -1209,7 +1238,7 @@
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 +1318,12 @@
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,
@@ -1456,6 +1490,9 @@
final String ifname = intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME);
final int ipmode = intent.getIntExtra(EXTRA_WIFI_AP_MODE, IFACE_IP_MODE_UNSPECIFIED);
+ // In B+, Tethered AP is handled by StartTetheringSoftApCallback.
+ if (isTetheringWithSoftApConfigEnabled() && ipmode == IFACE_IP_MODE_TETHERED) return;
+
switch (curState) {
case WifiManager.WIFI_AP_STATE_ENABLING:
// We can see this state on the way to both enabled and failure states.
@@ -1536,12 +1573,64 @@
}
}
+ class StartTetheringSoftApCallback implements SoftApCallback {
+
+ @Nullable
+ IIntResultListener mPendingListener;
+
+ StartTetheringSoftApCallback(IIntResultListener pendingListener) {
+ mPendingListener = pendingListener;
+ }
+
+ @Override
+ public void onStateChanged(SoftApState softApState) {
+ final int state = softApState.getState();
+ final String iface = softApState.getIface();
+ final TetheringRequest request = softApState.getTetheringRequest();
+ switch (softApState.getState()) {
+ case WifiManager.WIFI_AP_STATE_ENABLED:
+ enableIpServing(request, iface);
+ // If stopTethering has already been called, IP serving will still be started,
+ // but as soon as the wifi code processes the stop, WIFI_AP_STATE_DISABLED will
+ // be sent and tethering will be stopped again.
+ sendTetherResultAndRemoveOnError(request, mPendingListener,
+ TETHER_ERROR_NO_ERROR);
+ mPendingListener = null;
+ break;
+ case WifiManager.WIFI_AP_STATE_FAILED:
+ // TODO: if a call to startTethering happens just after a call to stopTethering,
+ // the start will fail because hotspot is still being disabled. This likely
+ // cannot be fixed in tethering code but must be fixed in WiFi.
+ sendTetherResultAndRemoveOnError(request, mPendingListener,
+ TETHER_ERROR_INTERNAL_ERROR);
+ mPendingListener = null;
+ break;
+ case WifiManager.WIFI_AP_STATE_DISABLED:
+ // TODO(b/403164072): SoftAP may restart due to CC change, in which we'll get
+ // DISABLED -> ENABLED (or FAILED). Before the transition back to ENABLED is
+ // complete, it is possible that a new Wifi request is accepted since there's no
+ // active request to fuzzy-match it, which will unexpectedly cause Wifi to
+ // overwrite this SoftApCallback. This should be fixed in Wifi to disallow any
+ // new calls to startTetheredHotspot while SoftAP is restarting.
+ disableWifiIpServing(iface, state);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
@VisibleForTesting
List<TetheringRequest> getPendingTetheringRequests() {
return mRequestTracker.getPendingTetheringRequests();
}
@VisibleForTesting
+ List<TetheringRequest> getServingTetheringRequests() {
+ return mRequestTracker.getServingTetheringRequests();
+ }
+
+ @VisibleForTesting
boolean isTetheringActive() {
return getTetheredIfaces().length > 0;
}
@@ -1611,7 +1700,7 @@
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 +1717,7 @@
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 +1864,7 @@
void setUsbTethering(boolean enable, IIntResultListener listener) {
mHandler.post(() -> {
- try {
- listener.onResult(setUsbTethering(enable));
- } catch (RemoteException e) { }
+ sendTetherResult(listener, setUsbTethering(enable));
});
}
@@ -2904,6 +2991,9 @@
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 +3008,19 @@
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 +3116,18 @@
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 +3135,7 @@
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/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index bd35cf2..00a7f09 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -37,7 +37,6 @@
import androidx.annotation.RequiresApi;
import com.android.modules.utils.build.SdkLevel;
-import com.android.net.flags.Flags;
import com.android.net.module.util.RoutingCoordinatorManager;
import com.android.net.module.util.RoutingCoordinatorService;
import com.android.net.module.util.SharedLog;
@@ -211,9 +210,9 @@
}
/**
- * Wrapper for tethering_with_soft_ap_config feature flag.
+ * Returns true if the tethering with soft ap config feature is enabled.
*/
public boolean isTetheringWithSoftApConfigEnabled() {
- return SdkLevel.isAtLeastB() && Flags.tetheringWithSoftApConfig();
+ return SdkLevel.isAtLeastB();
}
}
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 e00e9f0..086f2d2 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 @@
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 @@
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 @@
@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 @@
@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 @@
@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 @@
}
@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 @@
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 @@
@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 @@
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 fe3b201..dc3cbd2 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -50,15 +50,18 @@
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;
@@ -66,6 +69,7 @@
import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_FAILED;
import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@@ -166,6 +170,7 @@
import android.net.ip.IpServer;
import android.net.ip.RouterAdvertisementDaemon;
import android.net.wifi.SoftApConfiguration;
+import android.net.wifi.SoftApState;
import android.net.wifi.WifiClient;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.SoftApCallback;
@@ -238,6 +243,7 @@
import java.util.List;
import java.util.Set;
import java.util.Vector;
+import java.util.concurrent.Executor;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -308,8 +314,7 @@
@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();
@@ -340,6 +345,7 @@
private TestConnectivityManager mCm;
private boolean mForceEthernetServiceUnavailable = false;
private int mBinderCallingUid = TEST_CALLER_UID;
+ private boolean mTetheringWithSoftApConfigEnabled = SdkLevel.isAtLeastB();
private class TestContext extends BroadcastInterceptingContext {
TestContext(Context base) {
@@ -396,6 +402,9 @@
}
public class MockIpServerDependencies extends IpServer.Dependencies {
+
+ private int mOnDhcpServerCreatedResult = STATUS_SUCCESS;
+
@Override
public DadProxy getDadProxy(
Handler handler, InterfaceParams ifParams) {
@@ -437,7 +446,7 @@
DhcpServerCallbacks cb) {
new Thread(() -> {
try {
- cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer);
+ cb.onDhcpServerCreated(mOnDhcpServerCreatedResult, mDhcpServer);
} catch (RemoteException e) {
fail(e.getMessage());
}
@@ -448,6 +457,10 @@
IpNeighborMonitor.NeighborEventConsumer c) {
return mIpNeighborMonitor;
}
+
+ public void setOnDhcpServerCreatedResult(final int result) {
+ mOnDhcpServerCreatedResult = result;
+ }
}
public class MockTetheringDependencies extends TetheringDependencies {
@@ -573,6 +586,11 @@
public int getBinderCallingUid() {
return mBinderCallingUid;
}
+
+ @Override
+ public boolean isTetheringWithSoftApConfigEnabled() {
+ return mTetheringWithSoftApConfigEnabled;
+ }
}
private static LinkProperties buildUpstreamLinkProperties(String interfaceName,
@@ -708,6 +726,9 @@
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
+ mIpServerDependencies = spy(new MockIpServerDependencies());
+ when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
+ mTetheringWithSoftApConfigEnabled = SdkLevel.isAtLeastB();
}
// In order to interact with syncSM from the test, tethering must be created in test thread.
@@ -824,6 +845,38 @@
mLooper.dispatchAll();
}
+ private void sendStartTetheringSoftApCallback(int state, TetheringRequest request,
+ String ifname) {
+ ArgumentCaptor<SoftApCallback> callbackCaptor =
+ ArgumentCaptor.forClass(SoftApCallback.class);
+ verify(mWifiManager, atLeastOnce()).startTetheredHotspot(any(TetheringRequest.class),
+ any(Executor.class), callbackCaptor.capture());
+ SoftApState softApState = mock(SoftApState.class);
+ when(softApState.getState()).thenReturn(state);
+ when(softApState.getTetheringRequest()).thenReturn(request);
+ when(softApState.getIface()).thenReturn(ifname);
+ callbackCaptor.getValue().onStateChanged(softApState);
+ mLooper.dispatchAll();
+ }
+
+ private void verifyWifiTetheringRequested() {
+ if (mTetheringDependencies.isTetheringWithSoftApConfigEnabled()) {
+ verify(mWifiManager).startTetheredHotspot(any(), any(), any());
+ } else {
+ verify(mWifiManager).startTetheredHotspot(null);
+ }
+ verify(mWifiManager, never()).stopSoftAp();
+ verifyNoMoreInteractions(mWifiManager);
+ }
+
+ private void sendSoftApEvent(int state, TetheringRequest request, String ifname) {
+ if (mTetheringDependencies.isTetheringWithSoftApConfigEnabled()) {
+ sendStartTetheringSoftApCallback(state, request, ifname);
+ } else {
+ sendWifiApStateChanged(state, ifname, IFACE_IP_MODE_TETHERED);
+ }
+ }
+
private static final String[] P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST = {
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_WIFI_STATE
@@ -1944,14 +1997,11 @@
@Test
public void failingWifiTetheringLegacyApBroadcast() throws Exception {
initTetheringOnTestThread();
- when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
// Emulate pressing the WiFi tethering button.
- mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
- null);
+ mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG, null);
mLooper.dispatchAll();
- verify(mWifiManager, times(1)).startTetheredHotspot(null);
- verifyNoMoreInteractions(mWifiManager);
+ verifyWifiTetheringRequested();
verifyNoMoreInteractions(mNetd);
// Emulate externally-visible WifiManager effects, causing the
@@ -1980,15 +2030,15 @@
// TODO: Test with and without interfaceStatusChanged().
@Test
public void workingWifiTetheringEnrichedApBroadcast() throws Exception {
+ // B+ uses SoftApCallback instead of WIFI_AP_STATE_CHANGED for tethered hotspot.
+ mTetheringWithSoftApConfigEnabled = false;
initTetheringOnTestThread();
- when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
// Emulate pressing the WiFi tethering button.
mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
null);
mLooper.dispatchAll();
- verify(mWifiManager, times(1)).startTetheredHotspot(null);
- verifyNoMoreInteractions(mWifiManager);
+ verifyWifiTetheringRequested();
verifyNoMoreInteractions(mNetd);
// Emulate externally-visible WifiManager effects, causing the
@@ -2026,19 +2076,133 @@
verifyStopHotpot();
}
+ @Test
+ public void startWifiTetheringWithSoftApConfigurationSuccess() throws Exception {
+ assumeTrue(mTetheringDependencies.isTetheringWithSoftApConfigEnabled());
+ initTetheringOnTestThread();
+
+ // Emulate pressing the WiFi tethering button.
+ TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes("SSID".getBytes(StandardCharsets.UTF_8)))
+ .build())
+ .build();
+ IIntResultListener startResultListener = mock(IIntResultListener.class);
+ mTethering.startTethering(request, TEST_CALLER_PKG, startResultListener);
+ mLooper.dispatchAll();
+ verifyNoMoreInteractions(mNetd);
+ verify(startResultListener, never()).onResult(anyInt());
+ // Emulate Wifi iface enabled
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, request, TEST_WLAN_IFNAME);
+
+ verifyStartHotspot();
+ verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_TETHER);
+ verify(startResultListener).onResult(TETHER_ERROR_NO_ERROR);
+ }
+
+ @Test
+ public void startWifiTetheringWithSoftApConfigurationFailure() throws Exception {
+ assumeTrue(mTetheringDependencies.isTetheringWithSoftApConfigEnabled());
+ initTetheringOnTestThread();
+
+ // Emulate pressing the WiFi tethering button.
+ TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes("SSID".getBytes(StandardCharsets.UTF_8)))
+ .build())
+ .build();
+ IIntResultListener startResultListener = mock(IIntResultListener.class);
+ mTethering.startTethering(request, TEST_CALLER_PKG, startResultListener);
+ mLooper.dispatchAll();
+ verify(startResultListener, never()).onResult(anyInt());
+ // Emulate Wifi iface failure
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_FAILED, request, TEST_WLAN_IFNAME);
+
+ verify(startResultListener).onResult(TETHER_ERROR_INTERNAL_ERROR);
+ }
+
+ @Test
+ public void startWifiTetheringWithSoftApConfigurationRestartAfterStarting() throws Exception {
+ assumeTrue(mTetheringDependencies.isTetheringWithSoftApConfigEnabled());
+ initTetheringOnTestThread();
+ TestTetheringEventCallback callback = new TestTetheringEventCallback();
+ SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes("SSID".getBytes(StandardCharsets.UTF_8)))
+ .build();
+ final TetheringInterface wifiIface = new TetheringInterface(
+ TETHERING_WIFI, TEST_WLAN_IFNAME);
+ final TetheringInterface wifiIfaceWithConfig = new TetheringInterface(
+ TETHERING_WIFI, TEST_WLAN_IFNAME, softApConfig);
+
+ // 1. Register one callback before running any tethering.
+ mTethering.registerTetheringEventCallback(callback);
+ mLooper.dispatchAll();
+ assertTetherStatesNotNullButEmpty(callback.pollTetherStatesChanged());
+ // Emulate pressing the WiFi tethering button.
+ TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(softApConfig)
+ .build();
+ IIntResultListener startResultListener = mock(IIntResultListener.class);
+ mTethering.startTethering(request, TEST_CALLER_PKG, startResultListener);
+ mLooper.dispatchAll();
+
+ // Wifi success
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, request, TEST_WLAN_IFNAME);
+ verifyStartHotspot();
+ TetherStatesParcel tetherState = callback.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
+ tetherState = callback.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIfaceWithConfig});
+ verify(startResultListener).onResult(TETHER_ERROR_NO_ERROR);
+
+ // Restart Wifi
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_DISABLED, request, TEST_WLAN_IFNAME);
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, request, TEST_WLAN_IFNAME);
+
+ // Verify we go from TETHERED -> AVAILABLE -> TETHERED with the same config.
+ tetherState = callback.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
+ tetherState = callback.pollTetherStatesChanged();
+ assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIfaceWithConfig});
+ }
+
+ @Test
+ public void startWifiApBroadcastDoesNotStartIpServing() throws Exception {
+ assumeTrue(mTetheringDependencies.isTetheringWithSoftApConfigEnabled());
+ initTetheringOnTestThread();
+
+ // Call startTethering for wifi
+ TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes("SSID".getBytes(StandardCharsets.UTF_8)))
+ .build())
+ .build();
+ IIntResultListener startResultListener = mock(IIntResultListener.class);
+ mTethering.startTethering(request, TEST_CALLER_PKG, startResultListener);
+ mLooper.dispatchAll();
+
+ // WIFI_AP_STATE_CHANGED broadcast should be ignored since we should only be using
+ // SoftApCallback for tethered AP.
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ verify(mNetd, never()).tetherStartWithConfiguration(any());
+ verify(mNotificationUpdater, never()).onDownstreamChanged(DOWNSTREAM_NONE);
+ verify(mWifiManager, never()).updateInterfaceIpState(
+ TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ assertTrue(mTethering.getServingTetheringRequests().isEmpty());
+ }
+
// TODO: Test with and without interfaceStatusChanged().
@Test
public void failureEnablingIpForwarding() throws Exception {
initTetheringOnTestThread();
- when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
// Emulate pressing the WiFi tethering button.
- mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
- null);
+ TetheringRequest request = createTetheringRequest(TETHERING_WIFI);
+ mTethering.startTethering(request, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
- verify(mWifiManager, times(1)).startTetheredHotspot(null);
- verifyNoMoreInteractions(mWifiManager);
+ verifyWifiTetheringRequested();
verifyNoMoreInteractions(mNetd);
verify(mTetheringMetrics).createBuilder(eq(TETHERING_WIFI), anyString());
@@ -2046,7 +2210,7 @@
// per-interface state machine to start up, and telling us that
// tethering mode is to be started.
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
- sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ sendSoftApEvent(WIFI_AP_STATE_ENABLED, request, TEST_WLAN_IFNAME);
// We verify get/set called three times here: twice for setup and once during
// teardown because all events happen over the course of the single
@@ -2374,9 +2538,9 @@
// 2. Enable wifi tethering.
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
initTetheringUpstream(upstreamState);
- when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
- mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
+ TetheringRequest request = createTetheringRequest(TETHERING_WIFI);
+ mTethering.startTethering(request, TEST_CALLER_PKG,
null);
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
mLooper.dispatchAll();
@@ -2384,7 +2548,7 @@
// Starting in B, ignore the interfaceStatusChanged
callback.assertNoStateChangeCallback();
}
- sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ sendSoftApEvent(WIFI_AP_STATE_ENABLED, request, TEST_WLAN_IFNAME);
mLooper.dispatchAll();
tetherState = callback.pollTetherStatesChanged();
assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
@@ -2412,8 +2576,7 @@
if (isAtLeastT()) {
// After T, tethering doesn't support WIFI_AP_STATE_DISABLED with null interface name.
callback2.assertNoStateChangeCallback();
- sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME,
- IFACE_IP_MODE_TETHERED);
+ sendSoftApEvent(WIFI_AP_STATE_DISABLED, request, TEST_WLAN_IFNAME);
}
tetherState = callback2.pollTetherStatesChanged();
assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
@@ -2425,7 +2588,7 @@
@Test
public void testSoftApConfigInTetheringEventCallback() throws Exception {
- assumeTrue(SdkLevel.isAtLeastV());
+ assumeTrue(mTetheringDependencies.isTetheringWithSoftApConfigEnabled());
when(mContext.checkCallingOrSelfPermission(NETWORK_SETTINGS))
.thenReturn(PERMISSION_DENIED);
when(mContext.checkCallingOrSelfPermission(NETWORK_STACK))
@@ -2477,19 +2640,17 @@
callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED);
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
initTetheringUpstream(upstreamState);
- when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
// 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();
- }
- sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
- mLooper.dispatchAll();
+ // Netd "up" event should not trigger a state change callback in B+.
+ callback.assertNoStateChangeCallback();
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, tetheringRequest,
+ TEST_WLAN_IFNAME);
// Verify we see Available -> Tethered states
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
callback.pollTetherStatesChanged().availableList);
@@ -2507,20 +2668,15 @@
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,
- IFACE_IP_MODE_TETHERED);
- }
- assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
+ mLooper.dispatchAll();
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_DISABLED, tetheringRequest,
+ TEST_WLAN_IFNAME);
+ 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);
@@ -2529,6 +2685,340 @@
}
@Test
+ public void testFuzzyMatchedWifiCannotBeAdded() throws Exception {
+ assumeTrue(mTetheringDependencies.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);
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener);
+ mLooper.dispatchAll();
+ successListener.assertDoesNotHaveResult();
+
+ // 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();
+ verifyWifiTetheringRequested();
+ 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();
+ verifyWifiTetheringRequested();
+
+ // Mock the link layer event to start IP serving and verify
+ // 1) The original request's result listener is called.
+ // 2) We still get TETHER_ERROR_DUPLICATE_REQUEST for new requests.
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, tetheringRequest, TEST_WLAN_IFNAME);
+ successListener.assertHasResult();
+ 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(), any(), any());
+ verify(mWifiManager, never()).stopSoftAp();
+ }
+
+ @Test
+ public void testFuzzyMatchedWifiCanBeAddedAfterIpServerStopped() throws Exception {
+ assumeTrue(mTetheringDependencies.isTetheringWithSoftApConfigEnabled());
+ 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();
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, tetheringRequest, TEST_WLAN_IFNAME);
+ 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();
+
+ // 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(mTetheringDependencies.isTetheringWithSoftApConfigEnabled());
+ 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();
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, tetheringRequest, TEST_WLAN_IFNAME);
+ 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();
+
+ // Trigger wifi ap state change to tell IpServer it's unwanted.
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_DISABLED, tetheringRequest,
+ TEST_WLAN_IFNAME);
+
+ // 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();
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, tetheringRequest,
+ TEST_WLAN_IFNAME);
+ successListener2.assertHasResult();
+ }
+
+ @Test
+ public void testFuzzyMatchedWifiCanBeAddedAfterIpServerError() throws Exception {
+ assumeTrue(mTetheringDependencies.isTetheringWithSoftApConfigEnabled());
+ 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();
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, tetheringRequest, TEST_WLAN_IFNAME);
+ successListener.assertHasResult();
+
+ // 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();
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, tetheringRequest, TEST_WLAN_IFNAME);
+ successListener2.assertHasResult();
+ }
+
+ @Test
+ public void testFuzzyMatchedWifiCanBeAddedAfterStoppingPendingRequest() throws Exception {
+ assumeTrue(mTetheringDependencies.isTetheringWithSoftApConfigEnabled());
+ 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();
+ ArgumentCaptor<SoftApCallback> callbackCaptor =
+ ArgumentCaptor.forClass(SoftApCallback.class);
+ verify(mWifiManager, atLeastOnce()).startTetheredHotspot(any(TetheringRequest.class),
+ any(Executor.class), callbackCaptor.capture());
+
+ // 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();
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, tetheringRequest, TEST_WLAN_IFNAME);
+ successListener2.assertHasResult();
+
+ // Mock the first request going up and then down from the stop request.
+ SoftApState softApState = mock(SoftApState.class);
+ when(softApState.getState()).thenReturn(WIFI_AP_STATE_ENABLED);
+ when(softApState.getTetheringRequest()).thenReturn(tetheringRequest);
+ when(softApState.getIface()).thenReturn(TEST_WLAN_IFNAME);
+ callbackCaptor.getValue().onStateChanged(softApState);
+ mLooper.dispatchAll();
+ successListener.assertHasResult();
+
+ // Mock the second request going up
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, tetheringRequest, TEST_WLAN_IFNAME);
+ successListener2.assertHasResult();
+ }
+
+ @Test
+ public void testFuzzyMatchedWifiCanBeAddedAfterStoppingServingRequest() throws Exception {
+ assumeTrue(mTetheringDependencies.isTetheringWithSoftApConfigEnabled());
+ 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();
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, tetheringRequest, TEST_WLAN_IFNAME);
+ 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();
+ sendStartTetheringSoftApCallback(WIFI_AP_STATE_ENABLED, tetheringRequest, TEST_WLAN_IFNAME);
+ successListener2.assertHasResult();
+ }
+
+ @Test
+ public void testStopTetheringWithMatchingRequest() throws Exception {
+ assumeTrue(mTetheringDependencies.isTetheringWithSoftApConfigEnabled());
+ when(mContext.checkCallingOrSelfPermission(NETWORK_SETTINGS)).thenReturn(PERMISSION_DENIED);
+ initTetheringOnTestThread();
+ UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
+ initTetheringUpstream(upstreamState);
+
+ // 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(mTetheringDependencies.isTetheringWithSoftApConfigEnabled());
+ initTetheringOnTestThread();
+ UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
+ initTetheringUpstream(upstreamState);
+
+ // 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();
final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
@@ -2794,7 +3284,8 @@
}
@Test
- public void testMultipleStartTethering() throws Exception {
+ public void testMultipleStartTetheringLegacy() throws Exception {
+ mTetheringWithSoftApConfigEnabled = false;
initTetheringOnTestThread();
final LinkAddress serverLinkAddr = new LinkAddress("192.168.20.1/24");
final LinkAddress clientLinkAddr = new LinkAddress("192.168.20.42/24");
@@ -2891,12 +3382,11 @@
final int clientAddrParceled = 0xc0a8002a;
final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
- when(mWifiManager.startTetheredHotspot(any())).thenReturn(true);
mTethering.startTethering(createTetheringRequest(TETHERING_WIFI,
serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL, null),
TEST_CALLER_PKG, null);
mLooper.dispatchAll();
- verify(mWifiManager, times(1)).startTetheredHotspot(any());
+ verifyWifiTetheringRequested();
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
// Call legacyTether on the interface before the link layer event comes back.
@@ -3055,26 +3545,38 @@
mLooper.dispatchAll();
verify(mEntitleMgr).stopProvisioningIfNeeded(TETHERING_WIFI);
reset(mEntitleMgr);
+ }
- // If one app enables tethering without provisioning check first, then another app enables
- // tethering of the same type but does not disable the provisioning check.
+ @Test
+ public void testNonExemptRequestAddedAfterExemptRequestOfSameType() throws Exception {
+ // Note: When fuzzy-matching is enabled, it is not possible yet to have two concurrent
+ // requests of the same type that are subject to carrier entitlement due to fuzzy-matching.
+ mTetheringWithSoftApConfigEnabled = false;
+ initTetheringOnTestThread();
setupForRequiredProvisioning();
+ final TetheringRequest wifiExemptRequest =
+ createTetheringRequest(TETHERING_WIFI, null, null, true,
+ CONNECTIVITY_SCOPE_GLOBAL, null);
mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
assertTrue(mEntitleMgr.isCellularUpstreamPermitted());
reset(mEntitleMgr);
+
setupForRequiredProvisioning();
+ final TetheringRequest wifiNotExemptRequest =
+ createTetheringRequest(TETHERING_WIFI, null, null, false,
+ CONNECTIVITY_SCOPE_GLOBAL, null);
mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
+ verify(mEntitleMgr).stopProvisioningIfNeeded(TETHERING_WIFI);
verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
assertFalse(mEntitleMgr.isCellularUpstreamPermitted());
mTethering.stopTethering(TETHERING_WIFI);
mLooper.dispatchAll();
- verify(mEntitleMgr).stopProvisioningIfNeeded(TETHERING_WIFI);
- reset(mEntitleMgr);
+ verify(mEntitleMgr, times(2)).stopProvisioningIfNeeded(TETHERING_WIFI);
}
private void setupForRequiredProvisioning() {
@@ -3256,8 +3758,11 @@
reset(mDhcpServer);
// Run wifi tethering.
+ TetheringRequest request = createTetheringRequest(TETHERING_WIFI);
+ mTethering.startTethering(request, TEST_CALLER_PKG, null);
+ mLooper.dispatchAll();
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
- sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ sendSoftApEvent(WIFI_AP_STATE_ENABLED, request, TEST_WLAN_IFNAME);
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
any(), dhcpEventCbsCaptor.capture());
eventCallbacks = dhcpEventCbsCaptor.getValue();
@@ -3320,8 +3825,12 @@
});
callback.expectTetheredClientChanged(Collections.emptyList());
+ TetheringRequest request = createTetheringRequest(TETHERING_WIFI);
+ mTethering.startTethering(request, TEST_CALLER_PKG, null);
+ mLooper.dispatchAll();
+ verifyWifiTetheringRequested();
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
- sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ sendSoftApEvent(WIFI_AP_STATE_ENABLED, request, TEST_WLAN_IFNAME);
final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
ArgumentCaptor.forClass(IDhcpEventCallbacks.class);
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
@@ -3575,6 +4084,7 @@
@Test
public void testStartBluetoothTetheringFailsWhenTheresAnExistingRequestWaitingForPanService()
throws Exception {
+ mTetheringWithSoftApConfigEnabled = false;
initTetheringOnTestThread();
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
@@ -3851,8 +4361,11 @@
@Test
public void testIpv4AddressForSapAndLohsConcurrency() throws Exception {
initTetheringOnTestThread();
+ TetheringRequest request = createTetheringRequest(TETHERING_WIFI);
+ mTethering.startTethering(request, TEST_CALLER_PKG, null);
+ mLooper.dispatchAll();
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
- sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ sendSoftApEvent(WIFI_AP_STATE_ENABLED, request, TEST_WLAN_IFNAME);
ArgumentCaptor<InterfaceConfigurationParcel> ifaceConfigCaptor =
ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
@@ -3873,6 +4386,21 @@
}
@Test
+ public void testFailStartTetheredHotspotWithoutRequest() throws Exception {
+ mTetheringWithSoftApConfigEnabled = false;
+ initTetheringOnTestThread();
+ when(mWifiManager.startTetheredHotspot(null)).thenReturn(false);
+
+ ResultListener result = new ResultListener(TETHER_ERROR_INTERNAL_ERROR);
+ mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG, result);
+ mLooper.dispatchAll();
+ verify(mWifiManager).startTetheredHotspot(null);
+ verifyNoMoreInteractions(mWifiManager);
+ result.assertHasResult();
+ assertTrue(mTethering.getPendingTetheringRequests().isEmpty());
+ }
+
+ @Test
public void testWifiTetheringWhenP2pActive() throws Exception {
initTetheringOnTestThread();
// Enable wifi P2P.
@@ -3885,16 +4413,14 @@
verify(mUpstreamNetworkMonitor, never()).setTryCell(true);
assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastErrorForTest(TEST_P2P_IFNAME));
- when(mWifiManager.startTetheredHotspot(any())).thenReturn(true);
// Emulate pressing the WiFi tethering button.
- mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
- null);
+ TetheringRequest request = createTetheringRequest(TETHERING_WIFI);
+ mTethering.startTethering(request, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
- verify(mWifiManager).startTetheredHotspot(null);
- verifyNoMoreInteractions(mWifiManager);
+ verifyWifiTetheringRequested();
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
- sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ sendSoftApEvent(WIFI_AP_STATE_ENABLED, request, TEST_WLAN_IFNAME);
verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
verify(mWifiManager).updateInterfaceIpState(
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 4f18fa2..3d7ea69 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -1252,13 +1252,9 @@
* {@link ConnectivityManager#registerNetworkAgent}
* @hide
*/
- public static NetworkAndAgentRegistryParcelable registerNetworkAgentResult(
+ public static Network registerNetworkAgentResult(
@Nullable final Network network, @Nullable final INetworkAgentRegistry registry) {
- final NetworkAndAgentRegistryParcelable result =
- new NetworkAndAgentRegistryParcelable();
- result.network = network;
- result.registry = registry;
- return result;
+ return network;
}
}
@@ -3972,8 +3968,7 @@
@RequiresPermission(anyOf = {
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
android.Manifest.permission.NETWORK_FACTORY})
- public NetworkAndAgentRegistryParcelable registerNetworkAgent(
- @NonNull INetworkAgent na, @NonNull NetworkInfo ni,
+ public Network registerNetworkAgent(@NonNull INetworkAgent na, @NonNull NetworkInfo ni,
@NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
@NonNull NetworkScore score, @NonNull NetworkAgentConfig config, int providerId) {
return registerNetworkAgent(na, ni, lp, nc, null /* localNetworkConfig */, score, config,
@@ -3988,8 +3983,7 @@
@RequiresPermission(anyOf = {
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
android.Manifest.permission.NETWORK_FACTORY})
- public NetworkAndAgentRegistryParcelable registerNetworkAgent(
- @NonNull INetworkAgent na, @NonNull NetworkInfo ni,
+ public Network registerNetworkAgent(@NonNull INetworkAgent na, @NonNull NetworkInfo ni,
@NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
@Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
@NonNull NetworkAgentConfig config, int providerId) {
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index a270684..47b3316 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -30,7 +30,6 @@
import android.net.LocalNetworkConfig;
import android.net.Network;
import android.net.NetworkAgentConfig;
-import android.net.NetworkAndAgentRegistryParcelable;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
@@ -147,8 +146,7 @@
void declareNetworkRequestUnfulfillable(in NetworkRequest request);
- NetworkAndAgentRegistryParcelable registerNetworkAgent(
- in INetworkAgent na, in NetworkInfo ni, in LinkProperties lp,
+ Network registerNetworkAgent(in INetworkAgent na, in NetworkInfo ni, in LinkProperties lp,
in NetworkCapabilities nc, in NetworkScore score,
in LocalNetworkConfig localNetworkConfig, in NetworkAgentConfig config,
in int factorySerialNumber);
diff --git a/framework/src/android/net/INetworkAgent.aidl b/framework/src/android/net/INetworkAgent.aidl
index c6beeca..fa5175c 100644
--- a/framework/src/android/net/INetworkAgent.aidl
+++ b/framework/src/android/net/INetworkAgent.aidl
@@ -26,7 +26,7 @@
* @hide
*/
oneway interface INetworkAgent {
- void onRegistered();
+ void onRegistered(in INetworkAgentRegistry registry);
void onDisconnected();
void onBandwidthUpdateRequested();
void onValidationStatusChanged(int validationStatus,
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl
index afdd1ee..61b27b5 100644
--- a/framework/src/android/net/INetworkAgentRegistry.aidl
+++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -30,7 +30,7 @@
* Interface for NetworkAgents to send network properties.
* @hide
*/
-interface INetworkAgentRegistry {
+oneway interface INetworkAgentRegistry {
void sendNetworkCapabilities(in NetworkCapabilities nc);
void sendLinkProperties(in LinkProperties lp);
// TODO: consider replacing this by "markConnected()" and removing
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 08f5ecd..cefa1ea 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -98,7 +98,6 @@
@Nullable
private volatile Network mNetwork;
- // Null before the agent is registered
@Nullable
private volatile INetworkAgentRegistry mRegistry;
@@ -122,8 +121,6 @@
private NetworkInfo mNetworkInfo;
@NonNull
private final Object mRegisterLock = new Object();
- // TODO : move the preconnected queue to the system server and remove this
- private boolean mConnected = false;
/**
* The ID of the {@link NetworkProvider} that created this object, or
@@ -609,16 +606,16 @@
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_AGENT_CONNECTED: {
- // TODO : move the pre-connected queue to the system server, and remove
- // handling this EVENT_AGENT_CONNECTED message.
- synchronized (mPreConnectedQueue) {
- if (mConnected) {
- log("Received new connection while already connected!");
- } else {
- if (VDBG) log("NetworkAgent fully connected");
+ if (mRegistry != null) {
+ log("Received new connection while already connected!");
+ } else {
+ if (VDBG) log("NetworkAgent fully connected");
+ synchronized (mPreConnectedQueue) {
+ final INetworkAgentRegistry registry = (INetworkAgentRegistry) msg.obj;
+ mRegistry = registry;
for (RegistryAction a : mPreConnectedQueue) {
try {
- a.execute(mRegistry);
+ a.execute(registry);
} catch (RemoteException e) {
Log.wtf(LOG_TAG, "Communication error with registry", e);
// Fall through
@@ -626,7 +623,6 @@
}
mPreConnectedQueue.clear();
}
- mConnected = true;
}
break;
}
@@ -635,7 +631,7 @@
// let the client know CS is done with us.
onNetworkUnwanted();
synchronized (mPreConnectedQueue) {
- mConnected = false;
+ mRegistry = null;
}
break;
}
@@ -762,32 +758,20 @@
}
final ConnectivityManager cm = (ConnectivityManager) mInitialConfiguration.context
.getSystemService(Context.CONNECTIVITY_SERVICE);
- final NetworkAndAgentRegistryParcelable result;
if (mInitialConfiguration.localNetworkConfig == null) {
// Call registerNetworkAgent without localNetworkConfig argument to pass
// android.net.cts.NetworkAgentTest#testAgentStartsInConnecting in old cts
- result = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
+ mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
new NetworkInfo(mInitialConfiguration.info),
mInitialConfiguration.properties, mInitialConfiguration.capabilities,
mInitialConfiguration.score, mInitialConfiguration.config, providerId);
} else {
- result = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
+ mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
new NetworkInfo(mInitialConfiguration.info),
mInitialConfiguration.properties, mInitialConfiguration.capabilities,
mInitialConfiguration.localNetworkConfig, mInitialConfiguration.score,
mInitialConfiguration.config, providerId);
}
- if (null == result && Process.isApplicationUid(Process.myUid())) {
- // Let it slide in tests to allow mocking, since NetworkAndAgentRegistryParcelable
- // is not public and can't be instantiated by CTS. The danger here is that if
- // this happens in production for some reason the code will crash later instead
- // of here. If this is a system app, it will still crash as expected.
- Log.e(LOG_TAG, "registerNetworkAgent returned null. This agent will not work. "
- + "Is ConnectivityManager a mock ?");
- } else {
- mNetwork = result.network;
- mRegistry = result.registry;
- }
mInitialConfiguration = null; // All this memory can now be GC'd
}
return mNetwork;
@@ -803,8 +787,8 @@
}
@Override
- public void onRegistered() {
- mHandler.sendMessage(mHandler.obtainMessage(EVENT_AGENT_CONNECTED));
+ public void onRegistered(@NonNull INetworkAgentRegistry registry) {
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_AGENT_CONNECTED, registry));
}
@Override
@@ -929,13 +913,11 @@
*
* @hide
*/
- public INetworkAgent registerForTest(final Network network,
- final INetworkAgentRegistry registry) {
+ public INetworkAgent registerForTest(final Network network) {
log("Registering NetworkAgent for test");
synchronized (mRegisterLock) {
mNetwork = network;
mInitialConfiguration = null;
- mRegistry = registry;
}
return new NetworkAgentBinder(mHandler);
}
@@ -976,7 +958,7 @@
FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_MESSAGE_QUEUED_BEFORE_CONNECT
);
}
- if (mConnected) {
+ if (mRegistry != null) {
try {
action.execute(mRegistry);
} catch (RemoteException e) {
diff --git a/framework/src/android/net/NetworkAndAgentRegistryParcelable.aidl b/framework/src/android/net/NetworkAndAgentRegistryParcelable.aidl
deleted file mode 100644
index 8c01bbc..0000000
--- a/framework/src/android/net/NetworkAndAgentRegistryParcelable.aidl
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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 android.net;
-
-import android.net.INetworkAgentRegistry;
-import android.net.Network;
-
-/**
- * A pair of Network and NetworkAgentRegistry.
- *
- * {@hide}
- */
-@JavaDerive(toString=true)
-parcelable NetworkAndAgentRegistryParcelable {
- Network network;
- INetworkAgentRegistry registry;
-}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 555549c..4af8b0e 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -1942,7 +1942,7 @@
mContext, MdnsFeatureFlags.NSD_QUERY_WITH_KNOWN_ANSWER))
.setAvoidAdvertisingEmptyTxtRecords(mDeps.isTetheringFeatureNotChickenedOut(
mContext, MdnsFeatureFlags.NSD_AVOID_ADVERTISING_EMPTY_TXT_RECORDS))
- .setIsCachedServicesRemovalEnabled(mDeps.isFeatureEnabled(
+ .setIsCachedServicesRemovalEnabled(mDeps.isTetheringFeatureNotChickenedOut(
mContext, MdnsFeatureFlags.NSD_CACHED_SERVICES_REMOVAL))
.setCachedServicesRetentionTime(mDeps.getDeviceConfigPropertyInt(
MdnsFeatureFlags.NSD_CACHED_SERVICES_RETENTION_TIME,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 1cf5e4d..11a374d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -316,7 +316,7 @@
mIsAggressiveQueryModeEnabled = false;
mIsQueryWithKnownAnswerEnabled = false;
mAvoidAdvertisingEmptyTxtRecords = true; // Default enabled.
- mIsCachedServicesRemovalEnabled = false;
+ mIsCachedServicesRemovalEnabled = true; // Default enabled.
mCachedServicesRetentionTime = DEFAULT_CACHED_SERVICES_RETENTION_TIME_MILLISECONDS;
mIsAccurateDelayCallbackEnabled = false;
mIsShortHostnamesEnabled = true; // Default enabled.
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index 4ae8701..7c72fb1 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -78,6 +78,17 @@
return "CacheKey{ ServiceType=" + mUpperCaseServiceType + ", " + mSocketKey + " }";
}
}
+
+ public static class CachedService {
+ @NonNull final MdnsResponse mService;
+ boolean mServiceExpired;
+
+ CachedService(MdnsResponse service) {
+ mService = service;
+ mServiceExpired = false;
+ }
+ }
+
/**
* A map of cached services. Key is composed of service type and socket. Value is the list of
* services which are discovered from the given CacheKey.
@@ -86,7 +97,7 @@
* removal process to progress through the expiration check efficiently.
*/
@NonNull
- private final ArrayMap<CacheKey, List<MdnsResponse>> mCachedServices = new ArrayMap<>();
+ private final ArrayMap<CacheKey, List<CachedService>> mCachedServices = new ArrayMap<>();
/**
* A map of service expire callbacks. Key is composed of service type and socket and value is
* the callback listener.
@@ -113,6 +124,14 @@
mClock = clock;
}
+ private List<MdnsResponse> cachedServicesToResponses(List<CachedService> cachedServices) {
+ final List<MdnsResponse> responses = new ArrayList<>();
+ for (CachedService cachedService : cachedServices) {
+ responses.add(cachedService.mService);
+ }
+ return responses;
+ }
+
/**
* Get the cache services which are queried from given service type and socket.
*
@@ -126,7 +145,8 @@
maybeRemoveExpiredServices(cacheKey, mClock.elapsedRealtime());
}
return mCachedServices.containsKey(cacheKey)
- ? Collections.unmodifiableList(new ArrayList<>(mCachedServices.get(cacheKey)))
+ ? Collections.unmodifiableList(
+ cachedServicesToResponses(mCachedServices.get(cacheKey)))
: Collections.emptyList();
}
@@ -147,6 +167,16 @@
return null;
}
+ private static CachedService findMatchedCachedService(
+ @NonNull List<CachedService> cachedServices, @NonNull String serviceName) {
+ for (CachedService cachedService : cachedServices) {
+ if (equalsIgnoreDnsCase(serviceName, cachedService.mService.getServiceInstanceName())) {
+ return cachedService;
+ }
+ }
+ return null;
+ }
+
/**
* Get the cache service.
*
@@ -160,22 +190,23 @@
if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
maybeRemoveExpiredServices(cacheKey, mClock.elapsedRealtime());
}
- final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
- if (responses == null) {
+ final List<CachedService> cachedServices = mCachedServices.get(cacheKey);
+ if (cachedServices == null) {
return null;
}
- final MdnsResponse response = findMatchedResponse(responses, serviceName);
- return response != null ? new MdnsResponse(response) : null;
+ final CachedService cachedService = findMatchedCachedService(cachedServices, serviceName);
+ return cachedService != null ? new MdnsResponse(cachedService.mService) : null;
}
- static void insertResponseAndSortList(
- List<MdnsResponse> responses, MdnsResponse response, long now) {
+ static void insertServiceAndSortList(
+ List<CachedService> cachedServices, CachedService cachedService, long now) {
// binarySearch returns "the index of the search key, if it is contained in the list;
// otherwise, (-(insertion point) - 1)"
- final int searchRes = Collections.binarySearch(responses, response,
+ final int searchRes = Collections.binarySearch(cachedServices, cachedService,
// Sort the list by ttl.
- (o1, o2) -> Long.compare(o1.getMinRemainingTtl(now), o2.getMinRemainingTtl(now)));
- responses.add(searchRes >= 0 ? searchRes : (-searchRes - 1), response);
+ (o1, o2) -> Long.compare(o1.mService.getMinRemainingTtl(now),
+ o2.mService.getMinRemainingTtl(now)));
+ cachedServices.add(searchRes >= 0 ? searchRes : (-searchRes - 1), cachedService);
}
/**
@@ -186,20 +217,22 @@
*/
public void addOrUpdateService(@NonNull CacheKey cacheKey, @NonNull MdnsResponse response) {
ensureRunningOnHandlerThread(mHandler);
- final List<MdnsResponse> responses = mCachedServices.computeIfAbsent(
+ final List<CachedService> cachedServices = mCachedServices.computeIfAbsent(
cacheKey, key -> new ArrayList<>());
// Remove existing service if present.
- final MdnsResponse existing =
- findMatchedResponse(responses, response.getServiceInstanceName());
- responses.remove(existing);
+ final CachedService existing = findMatchedCachedService(cachedServices,
+ response.getServiceInstanceName());
+ cachedServices.remove(existing);
+
+ final CachedService cachedService = new CachedService(response);
if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
final long now = mClock.elapsedRealtime();
// Insert and sort service
- insertResponseAndSortList(responses, response, now);
+ insertServiceAndSortList(cachedServices, cachedService, now);
// Update the next expiration check time when a new service is added.
mNextExpirationTime = getNextExpirationTime(now);
} else {
- responses.add(response);
+ cachedServices.add(cachedService);
}
}
@@ -212,30 +245,30 @@
@Nullable
public MdnsResponse removeService(@NonNull String serviceName, @NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
- final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
- if (responses == null) {
+ final List<CachedService> cachedServices = mCachedServices.get(cacheKey);
+ if (cachedServices == null) {
return null;
}
- final Iterator<MdnsResponse> iterator = responses.iterator();
- MdnsResponse removedResponse = null;
+ final Iterator<CachedService> iterator = cachedServices.iterator();
+ CachedService removedService = null;
while (iterator.hasNext()) {
- final MdnsResponse response = iterator.next();
- if (equalsIgnoreDnsCase(serviceName, response.getServiceInstanceName())) {
+ final CachedService cachedService = iterator.next();
+ if (equalsIgnoreDnsCase(serviceName, cachedService.mService.getServiceInstanceName())) {
iterator.remove();
- removedResponse = response;
+ removedService = cachedService;
break;
}
}
if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
// Remove the serviceType if no response.
- if (responses.isEmpty()) {
+ if (cachedServices.isEmpty()) {
mCachedServices.remove(cacheKey);
}
// Update the next expiration check time when a service is removed.
mNextExpirationTime = getNextExpirationTime(mClock.elapsedRealtime());
}
- return removedResponse;
+ return removedService == null ? null : removedService.mService;
}
/**
@@ -288,24 +321,25 @@
mHandler.post(()-> callback.onServiceRecordExpired(previousResponse, newResponse));
}
- static List<MdnsResponse> removeExpiredServices(@NonNull List<MdnsResponse> responses,
+ static List<CachedService> removeExpiredServices(@NonNull List<CachedService> cachedServices,
long now) {
- final List<MdnsResponse> removedResponses = new ArrayList<>();
- final Iterator<MdnsResponse> iterator = responses.iterator();
+ final List<CachedService> removedServices = new ArrayList<>();
+ final Iterator<CachedService> iterator = cachedServices.iterator();
while (iterator.hasNext()) {
- final MdnsResponse response = iterator.next();
+ final CachedService cachedService = iterator.next();
// TODO: Check other records (A, AAAA, TXT) ttl time and remove the record if it's
// expired. Then send service update notification.
- if (!response.hasServiceRecord() || response.getMinRemainingTtl(now) > 0) {
+ if (!cachedService.mService.hasServiceRecord()
+ || cachedService.mService.getMinRemainingTtl(now) > 0) {
// The responses are sorted by the service record ttl time. Break out of loop
// early if service is not expired or no service record.
break;
}
// Remove the ttl expired service.
iterator.remove();
- removedResponses.add(response);
+ removedServices.add(cachedService);
}
- return removedResponses;
+ return removedServices;
}
private long getNextExpirationTime(long now) {
@@ -319,7 +353,7 @@
// The empty lists are not kept in the map, so there's always at least one
// element in the list. Therefore, it's fine to get the first element without a
// null check.
- mCachedServices.valueAt(i).get(0).getMinRemainingTtl(now));
+ mCachedServices.valueAt(i).get(0).mService.getMinRemainingTtl(now));
}
return minRemainingTtl == EXPIRATION_NEVER ? EXPIRATION_NEVER : now + minRemainingTtl;
}
@@ -334,24 +368,24 @@
return;
}
- final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
- if (responses == null) {
+ final List<CachedService> cachedServices = mCachedServices.get(cacheKey);
+ if (cachedServices == null) {
// No such services.
return;
}
- final List<MdnsResponse> removedResponses = removeExpiredServices(responses, now);
- if (removedResponses.isEmpty()) {
+ final List<CachedService> removedServices = removeExpiredServices(cachedServices, now);
+ if (removedServices.isEmpty()) {
// No expired services.
return;
}
- for (MdnsResponse previousResponse : removedResponses) {
- notifyServiceExpired(cacheKey, previousResponse, null /* newResponse */);
+ for (CachedService previousService : removedServices) {
+ notifyServiceExpired(cacheKey, previousService.mService, null /* newResponse */);
}
// Remove the serviceType if no response.
- if (responses.isEmpty()) {
+ if (cachedServices.isEmpty()) {
mCachedServices.remove(cacheKey);
}
@@ -368,8 +402,9 @@
for (int i = 0; i < mCachedServices.size(); i++) {
final CacheKey key = mCachedServices.keyAt(i);
pw.println(indent + key);
- for (MdnsResponse response : mCachedServices.valueAt(i)) {
- pw.println(indent + " Response{ " + response + " }");
+ for (CachedService cachedService : mCachedServices.valueAt(i)) {
+ pw.println(indent + " Response{ " + cachedService.mService
+ + " } Expired=" + cachedService.mServiceExpired);
}
pw.println();
}
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index c0082bb..622fba8 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -104,7 +104,7 @@
// First verify the clatd directory and binary,
// since this is built into the apex file system image,
// failures here are 99% likely to be build problems.
- V(kClatdDir, S_IFDIR|0750, ROOT, SYSTEM, "system_file", DIR);
+ V(kClatdDir, S_IFDIR|0750, CLAT, SYSTEM, "system_file", DIR);
V(kClatdBin, S_IFREG|S_ISUID|S_ISGID|0755, CLAT, CLAT, "clatd_exec", BIN);
// Move on to verifying that the bpf programs and maps are as expected.
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index a95d95c..3ce3f02 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -228,7 +228,6 @@
import android.net.Network;
import android.net.NetworkAgent;
import android.net.NetworkAgentConfig;
-import android.net.NetworkAndAgentRegistryParcelable;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState;
@@ -527,7 +526,6 @@
private final boolean mBackgroundFirewallChainEnabled;
private final boolean mUseDeclaredMethodsForCallbacksEnabled;
- private final boolean mQueueNetworkAgentEventsInSystemServer;
// Flag to delay callbacks for frozen apps, suppressing duplicate and stale callbacks.
private final boolean mQueueCallbacksForFrozenApps;
@@ -1930,9 +1928,6 @@
mUseDeclaredMethodsForCallbacksEnabled =
mDeps.isFeatureNotChickenedOut(context,
ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS);
- mQueueNetworkAgentEventsInSystemServer =
- mDeps.isFeatureNotChickenedOut(context,
- ConnectivityFlags.QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER);
// registerUidFrozenStateChangedCallback is only available on U+
mQueueCallbacksForFrozenApps = mDeps.isAtLeastU()
&& mDeps.isFeatureNotChickenedOut(context, QUEUE_CALLBACKS_FOR_FROZEN_APPS);
@@ -4693,30 +4688,18 @@
private void maybeHandleNetworkAgentMessage(Message msg) {
final Pair<NetworkAgentInfo, Object> arg = (Pair<NetworkAgentInfo, Object>) msg.obj;
final NetworkAgentInfo nai = arg.first;
-
- // If the network has been destroyed, the only thing that it can do is disconnect.
- if (nai.isDestroyed() && !isDisconnectRequest(msg)) {
- return;
- }
-
- if (mQueueNetworkAgentEventsInSystemServer && nai.maybeEnqueueMessage(msg)) {
- // If the message is enqueued, the NAI will replay it immediately
- // when registration is complete. It does this by sending all the
- // messages in the order received immediately after the
- // EVENT_AGENT_REGISTERED message.
- return;
- }
-
- // If the nai has been registered (and doesn't enqueue), it should now be
- // in the list of NAIs.
if (!mNetworkAgentInfos.contains(nai)) {
- // TODO : this is supposed to be impossible
if (VDBG) {
log(String.format("%s from unknown NetworkAgent", eventName(msg.what)));
}
return;
}
+ // If the network has been destroyed, the only thing that it can do is disconnect.
+ if (nai.isDestroyed() && !isDisconnectRequest(msg)) {
+ return;
+ }
+
switch (msg.what) {
case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: {
final NetworkCapabilities proposed = (NetworkCapabilities) arg.second;
@@ -9345,7 +9328,7 @@
* @param providerId the ID of the provider owning this NetworkAgent.
* @return the network created for this agent.
*/
- public NetworkAndAgentRegistryParcelable registerNetworkAgent(INetworkAgent na,
+ public Network registerNetworkAgent(INetworkAgent na,
NetworkInfo networkInfo,
LinkProperties linkProperties,
NetworkCapabilities networkCapabilities,
@@ -9388,8 +9371,7 @@
}
}
- private NetworkAndAgentRegistryParcelable registerNetworkAgentInternal(
- INetworkAgent na, NetworkInfo networkInfo,
+ private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
NetworkScore currentScore, NetworkAgentConfig networkAgentConfig,
@Nullable LocalNetworkConfig localNetworkConfig, int providerId,
@@ -9421,11 +9403,8 @@
// NetworkAgentInfo registration will finish when the NetworkMonitor is created.
// If the network disconnects or sends any other event before that, messages are deferred by
// NetworkAgent until nai.connect(), which will be called when finalizing the
- // registration. TODO : have NetworkAgentInfo defer them instead.
- final NetworkAndAgentRegistryParcelable result = new NetworkAndAgentRegistryParcelable();
- result.network = nai.network;
- result.registry = nai.getRegistry();
- return result;
+ // registration.
+ return nai.network;
}
private void handleRegisterNetworkAgent(NetworkAgentInfo nai, INetworkMonitor networkMonitor) {
@@ -9437,6 +9416,8 @@
nai.getDeclaredCapabilitiesSanitized(mCarrierPrivilegeAuthenticator)));
processLinkPropertiesFromAgent(nai, nai.linkProperties);
+ nai.onNetworkMonitorCreated(networkMonitor);
+
mNetworkAgentInfos.add(nai);
synchronized (mNetworkForNetId) {
mNetworkForNetId.put(nai.network.getNetId(), nai);
@@ -9451,7 +9432,7 @@
if (nai.isLocalNetwork()) {
handleUpdateLocalNetworkConfig(nai, null /* oldConfig */, nai.localNetworkConfig);
}
- nai.notifyRegistered(networkMonitor);
+ nai.notifyRegistered();
NetworkInfo networkInfo = nai.networkInfo;
updateNetworkInfo(nai, networkInfo);
updateVpnUids(nai, null, nai.networkCapabilities);
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 74bd235..136ea81 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -62,9 +62,6 @@
public static final String QUEUE_CALLBACKS_FOR_FROZEN_APPS =
"queue_callbacks_for_frozen_apps";
- public static final String QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER =
- "queue_network_agent_events_in_system_server";
-
private boolean mNoRematchAllRequestsOnRegister;
/**
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 4540f02..e762a8e 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -62,7 +62,6 @@
import android.net.TcpKeepalivePacketData;
import android.os.Handler;
import android.os.IBinder;
-import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.SystemClock;
@@ -631,7 +630,6 @@
// Used by ConnectivityService to keep track of 464xlat.
public final Nat464Xlat clatd;
- private final ArrayList<Message> mMessagesPendingRegistration = new ArrayList<>();
// Set after asynchronous creation of the NetworkMonitor.
private volatile NetworkMonitorManager mNetworkMonitor;
@@ -641,7 +639,6 @@
private final ConnectivityService.Dependencies mConnServiceDeps;
private final Context mContext;
private final Handler mHandler;
- private final NetworkAgentMessageHandler mRegistry;
private final QosCallbackTracker mQosCallbackTracker;
private final INetd mNetd;
@@ -676,7 +673,6 @@
mNetd = netd;
mContext = context;
mHandler = handler;
- mRegistry = new NetworkAgentMessageHandler(mHandler);
this.factorySerialNumber = factorySerialNumber;
this.creatorUid = creatorUid;
mLingerDurationMs = lingerDurationMs;
@@ -702,12 +698,10 @@
* Must be called from the ConnectivityService handler thread. A NetworkAgent can only be
* registered once.
*/
- public void notifyRegistered(final INetworkMonitor nm) {
- HandlerUtils.ensureRunningOnHandlerThread(mHandler);
- mNetworkMonitor = new NetworkMonitorManager(nm);
+ public void notifyRegistered() {
try {
networkAgent.asBinder().linkToDeath(mDeathMonitor, 0);
- networkAgent.onRegistered();
+ networkAgent.onRegistered(new NetworkAgentMessageHandler(mHandler));
} catch (RemoteException e) {
Log.e(TAG, "Error registering NetworkAgent", e);
maybeUnlinkDeathMonitor();
@@ -717,29 +711,6 @@
}
mHandler.obtainMessage(EVENT_AGENT_REGISTERED, ARG_AGENT_SUCCESS, 0, this).sendToTarget();
- for (final Message enqueued : mMessagesPendingRegistration) {
- mHandler.sendMessage(enqueued);
- }
- mMessagesPendingRegistration.clear();
- }
-
- /**
- * Enqueues a message if it needs to be enqueued, and returns whether it was enqueued.
- *
- * The message is enqueued iff it can't be sent just yet. If it can be sent
- * immediately, this method returns false and doesn't enqueue.
- *
- * If it enqueues, this method will make a copy of the message for enqueuing since
- * messages can't be reused or recycled before the end of their processing by the
- * handler.
- */
- public boolean maybeEnqueueMessage(final Message msg) {
- HandlerUtils.ensureRunningOnHandlerThread(mHandler);
- if (null != mNetworkMonitor) return false;
- final Message m = mHandler.obtainMessage();
- m.copyFrom(msg);
- mMessagesPendingRegistration.add(m);
- return true;
}
/**
@@ -1065,6 +1036,13 @@
}
/**
+ * Inform NetworkAgentInfo that a new NetworkMonitor was created.
+ */
+ public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) {
+ mNetworkMonitor = new NetworkMonitorManager(networkMonitor);
+ }
+
+ /**
* Set the NetworkCapabilities on this NetworkAgentInfo. Also attempts to notify NetworkMonitor
* of the new capabilities, if NetworkMonitor has been created.
*
@@ -1139,13 +1117,6 @@
return mNetworkMonitor;
}
- /**
- * Get the registry in this NetworkAgentInfo.
- */
- public INetworkAgentRegistry getRegistry() {
- return mRegistry;
- }
-
// Functions for manipulating the requests satisfied by this network.
//
// These functions must only called on ConnectivityService's main thread.
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 678e1ca..5e035a2 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -154,10 +154,10 @@
import java.security.MessageDigest
import java.time.Duration
import java.util.Arrays
+import java.util.Random
import java.util.UUID
import java.util.concurrent.Executors
import kotlin.collections.ArrayList
-import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
@@ -267,7 +267,7 @@
private class FakeConnectivityService {
val mockRegistry = mock(INetworkAgentRegistry::class.java)
private var agentField: INetworkAgent? = null
- val registry: INetworkAgentRegistry = object : INetworkAgentRegistry.Stub(),
+ private val registry = object : INetworkAgentRegistry.Stub(),
INetworkAgentRegistry by mockRegistry {
// asBinder has implementations in both INetworkAgentRegistry.Stub and mockRegistry, so
// it needs to be disambiguated. Just fail the test as it should be unused here.
@@ -284,7 +284,7 @@
fun connect(agent: INetworkAgent) {
this.agentField = agent
- agent.onRegistered()
+ agent.onRegistered(registry)
}
fun disconnect() = agent.onDisconnected()
@@ -413,8 +413,7 @@
}
private fun createNetworkAgentWithFakeCS() = createNetworkAgent().also {
- val binder = it.registerForTest(Network(FAKE_NET_ID), mFakeConnectivityService.registry)
- mFakeConnectivityService.connect(binder)
+ mFakeConnectivityService.connect(it.registerForTest(Network(FAKE_NET_ID)))
}
private fun TestableNetworkAgent.expectPostConnectionCallbacks(
@@ -1629,7 +1628,7 @@
val s = Os.socket(AF_INET6, SOCK_DGRAM, 0)
net.bindSocket(s)
val content = ByteArray(16)
- Random.nextBytes(content)
+ Random().nextBytes(content)
Os.sendto(s, ByteBuffer.wrap(content), 0, REMOTE_ADDRESS, 7 /* port */)
val match = reader.poll(DEFAULT_TIMEOUT_MS) {
val udpStart = IPV6_HEADER_LEN + UDP_HEADER_LEN
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index e645f67..7d6a213 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_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;
@@ -85,7 +86,6 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.modules.utils.build.SdkLevel;
-import com.android.net.flags.Flags;
import com.android.testutils.ParcelUtils;
import com.android.testutils.com.android.testutils.CarrierConfigRule;
@@ -234,6 +234,30 @@
}
+ @Test
+ public void testStartTetheringDuplicateRequestRejected() throws Exception {
+ assumeTrue(SdkLevel.isAtLeastB());
+ 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 +404,20 @@
tetherEventCallback.assumeWifiTetheringSupported(mContext);
tetherEventCallback.expectNoTetheringActive();
- SoftApConfiguration softApConfig = createSoftApConfiguration("SSID");
+ SoftApConfiguration softApConfig = SdkLevel.isAtLeastB()
+ ? createSoftApConfiguration("SSID") : null;
final TetheringInterface tetheredIface =
mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
assertNotNull(tetheredIface);
- assertEquals(softApConfig, tetheredIface.getSoftApConfiguration());
- final String wifiTetheringIface = tetheredIface.getInterface();
+ if (SdkLevel.isAtLeastB()) {
+ 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));
@@ -455,31 +482,75 @@
}
@Test
- public void testStopTetheringRequest() throws Exception {
- assumeTrue(isTetheringWithSoftApConfigEnabled());
+ public void testStopTetheringRequestNoMatchFailure() throws Exception {
+ assumeTrue(SdkLevel.isAtLeastB());
+ 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 {
+ assumeTrue(SdkLevel.isAtLeastB());
final TestTetheringEventCallback tetherEventCallback =
mCtsTetheringUtils.registerTetheringEventCallback();
try {
tetherEventCallback.assumeWifiTetheringSupported(mContext);
- // stopTethering without any tethering active should fail.
- TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI).build();
- mCtsTetheringUtils.stopTethering(request, false /* expectSuccess */);
+ SoftApConfiguration softApConfig = new SoftApConfiguration.Builder()
+ .setWifiSsid(WifiSsid.fromBytes("This is one config"
+ .getBytes(StandardCharsets.UTF_8))).build();
+ mCtsTetheringUtils.startWifiTethering(tetherEventCallback, softApConfig);
- // Start wifi tethering
- mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
-
- // stopTethering should succeed now that there's a request.
- mCtsTetheringUtils.stopTethering(request, true /* expectSuccess */);
+ // Stopping the active request should stop tethering.
+ TetheringRequest request = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(softApConfig)
+ .build();
+ mCtsTetheringUtils.stopTethering(request, true /* success */);
tetherEventCallback.expectNoTetheringActive();
} finally {
- mCtsTetheringUtils.stopAllTethering();
mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
}
}
- private boolean isTetheringWithSoftApConfigEnabled() {
- return SdkLevel.isAtLeastB() && Flags.tetheringWithSoftApConfig();
+ @Test
+ public void testStopTetheringRequestFuzzyMatchSuccess() throws Exception {
+ assumeTrue(SdkLevel.isAtLeastB());
+ 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.unregisterTetheringEventCallback(tetherEventCallback);
+ }
}
@Test
@@ -492,7 +563,7 @@
startTetheringCallback.expectTetheringFailed(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
// WRITE_SETTINGS not sufficient
- if (isTetheringWithSoftApConfigEnabled()) {
+ if (SdkLevel.isAtLeastB()) {
runAsShell(WRITE_SETTINGS, () -> {
mTM.startTethering(new TetheringRequest.Builder(TETHERING_WIFI).build(),
c -> c.run() /* executor */, startTetheringCallback);
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 3eefa0f..c28a0f8 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -2200,7 +2200,6 @@
case ConnectivityFlags.DELAY_DESTROY_SOCKETS:
case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
- case ConnectivityFlags.QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER:
return true;
default:
throw new UnsupportedOperationException("Unknown flag " + name
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index 976dfa9..2ebe87a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -21,6 +21,7 @@
import android.os.HandlerThread
import com.android.net.module.util.ArrayTrackRecord
import com.android.server.connectivity.mdns.MdnsServiceCache.CacheKey
+import com.android.server.connectivity.mdns.MdnsServiceCache.CachedService
import com.android.server.connectivity.mdns.MdnsServiceCacheTest.ExpiredRecord.ExpiredEvent.ServiceRecordExpired
import com.android.server.connectivity.mdns.util.MdnsUtils
import com.android.testutils.DevSdkIgnoreRule
@@ -289,32 +290,40 @@
@Test
fun testInsertResponseAndSortList() {
- val responses = ArrayList<MdnsResponse>()
- val response1 = createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 100L /* ttlTime */)
- MdnsServiceCache.insertResponseAndSortList(responses, response1, TEST_ELAPSED_REALTIME_MS)
- assertEquals(1, responses.size)
- assertEquals(response1, responses[0])
+ val services = ArrayList<CachedService>()
+ val service1 = CachedService(
+ createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 100L /* ttlTime */)
+ )
+ MdnsServiceCache.insertServiceAndSortList(services, service1, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(1, services.size)
+ assertEquals(service1, services[0])
- val response2 = createResponse(SERVICE_NAME_2, SERVICE_TYPE_1, 50L /* ttlTime */)
- MdnsServiceCache.insertResponseAndSortList(responses, response2, TEST_ELAPSED_REALTIME_MS)
- assertEquals(2, responses.size)
- assertEquals(response2, responses[0])
- assertEquals(response1, responses[1])
+ val service2 = CachedService(
+ createResponse(SERVICE_NAME_2, SERVICE_TYPE_1, 50L /* ttlTime */)
+ )
+ MdnsServiceCache.insertServiceAndSortList(services, service2, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(2, services.size)
+ assertEquals(service2, services[0])
+ assertEquals(service1, services[1])
- val response3 = createResponse(SERVICE_NAME_3, SERVICE_TYPE_1, 75L /* ttlTime */)
- MdnsServiceCache.insertResponseAndSortList(responses, response3, TEST_ELAPSED_REALTIME_MS)
- assertEquals(3, responses.size)
- assertEquals(response2, responses[0])
- assertEquals(response3, responses[1])
- assertEquals(response1, responses[2])
+ val service3 = CachedService(
+ createResponse(SERVICE_NAME_3, SERVICE_TYPE_1, 75L /* ttlTime */)
+ )
+ MdnsServiceCache.insertServiceAndSortList(services, service3, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(3, services.size)
+ assertEquals(service2, services[0])
+ assertEquals(service3, services[1])
+ assertEquals(service1, services[2])
- val response4 = createResponse("service-instance-4", SERVICE_TYPE_1, 125L /* ttlTime */)
- MdnsServiceCache.insertResponseAndSortList(responses, response4, TEST_ELAPSED_REALTIME_MS)
- assertEquals(4, responses.size)
- assertEquals(response2, responses[0])
- assertEquals(response3, responses[1])
- assertEquals(response1, responses[2])
- assertEquals(response4, responses[3])
+ val service4 = CachedService(
+ createResponse("service-instance-4", SERVICE_TYPE_1, 125L /* ttlTime */)
+ )
+ MdnsServiceCache.insertServiceAndSortList(services, service4, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(4, services.size)
+ assertEquals(service2, services[0])
+ assertEquals(service3, services[1])
+ assertEquals(service1, services[2])
+ assertEquals(service4, services[3])
}
@Test
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 48333c5..d7e781e 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -169,7 +169,6 @@
it[ConnectivityFlags.DELAY_DESTROY_SOCKETS] = true
it[ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS] = true
it[ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS] = true
- it[ConnectivityFlags.QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER] = true
}
fun setFeatureEnabled(flag: String, enabled: Boolean) = enabledFeatures.set(flag, enabled)
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index 40842f1..0c38f93 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -16,6 +16,9 @@
},
{
"name": "ThreadNetworkTrelDisabledTests"
+ },
+ {
+ "name": "ThreadBorderRouterIntegrationTests"
}
]
}
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
index 798a51e..8a72017 100644
--- a/thread/tests/integration/Android.bp
+++ b/thread/tests/integration/Android.bp
@@ -64,6 +64,26 @@
}
android_test {
+ name: "ThreadBorderRouterIntegrationTests",
+ platform_apis: true,
+ manifest: "AndroidManifest.xml",
+ test_config: "AndroidTestBorderRouter.xml",
+ defaults: [
+ "framework-connectivity-test-defaults",
+ "ThreadNetworkIntegrationTestsDefaults",
+ ],
+ test_suites: [
+ "mts-tethering",
+ "general-tests",
+ ],
+ srcs: [
+ "borderrouter/**/*.java",
+ "borderrouter/**/*.kt",
+ ],
+ compile_multilib: "both",
+}
+
+android_test {
name: "ThreadNetworkTrelDisabledTests",
platform_apis: true,
manifest: "AndroidManifest.xml",
diff --git a/thread/tests/integration/AndroidTestBorderRouter.xml b/thread/tests/integration/AndroidTestBorderRouter.xml
new file mode 100644
index 0000000..644e839
--- /dev/null
+++ b/thread/tests/integration/AndroidTestBorderRouter.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+
+<configuration description="Config for Thread Border Router integration tests">
+ <option name="test-tag" value="ThreadBorderRouterIntegrationTests" />
+ <option name="test-suite-tag" value="apct" />
+
+ <!--
+ Only run tests if the device under test is SDK version 34 (Android 14) or above.
+ -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.Sdk34ModuleController" />
+
+ <!-- Run tests in MTS only if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController">
+ <option name="required-feature" value="android.hardware.thread_network" />
+ </object>
+
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+
+ <!-- Install test -->
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="ThreadBorderRouterIntegrationTests.apk" />
+ <option name="check-min-sdk" value="true" />
+ <option name="cleanup-apks" value="true" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.thread.tests.integration" />
+ </test>
+
+ <!-- Enable TREL for integration tests -->
+ <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer">
+ <option name="flag-value"
+ value="thread_network/TrelFeature__enabled=true"/>
+ </target_preparer>
+</configuration>
diff --git a/thread/tests/integration/borderrouter/src/android/net/thread/borderrouter/BorderRouterIntegrationTest.java b/thread/tests/integration/borderrouter/src/android/net/thread/borderrouter/BorderRouterIntegrationTest.java
new file mode 100644
index 0000000..292079f
--- /dev/null
+++ b/thread/tests/integration/borderrouter/src/android/net/thread/borderrouter/BorderRouterIntegrationTest.java
@@ -0,0 +1,286 @@
+/*
+ * 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 android.net.thread;
+
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
+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.os.SystemClock.elapsedRealtime;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.LinkProperties;
+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 androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.collect.FluentIterable;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** Tests for E2E Border Router integration with ot-daemon, ConnectivityService, etc.. */
+@LargeTest
+@RequiresThreadFeature
+@RunWith(AndroidJUnit4.class)
+public class BorderRouterIntegrationTest {
+ // The maximum time for changes to be propagated to netdata.
+ private static final Duration NET_DATA_UPDATE_TIMEOUT = Duration.ofSeconds(1);
+
+ // The maximum time for OT addresses to be propagated to the TUN interface "thread-wpan"
+ private static final Duration TUN_ADDR_UPDATE_TIMEOUT = Duration.ofSeconds(1);
+
+ // The maximum time for changes in netdata to be propagated to link properties.
+ private static final Duration LINK_PROPERTIES_UPDATE_TIMEOUT = Duration.ofSeconds(1);
+
+ // The duration between attached and OMR address shows up on thread-wpan
+ private static final Duration OMR_LINK_ADDR_TIMEOUT = Duration.ofSeconds(30);
+
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+ private static final byte[] DEFAULT_DATASET_TLVS =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+ private static final ActiveOperationalDataset DEFAULT_DATASET =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+ private static final Inet6Address GROUP_ADDR_ALL_ROUTERS =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
+
+ private static final String TEST_NO_SLAAC_PREFIX = "9101:dead:beef:cafe::/64";
+ private static final InetAddress TEST_NO_SLAAC_PREFIX_ADDRESS =
+ InetAddresses.parseNumericAddress("9101:dead:beef:cafe::");
+
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+ private ExecutorService mExecutor;
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final ThreadNetworkControllerWrapper mController =
+ ThreadNetworkControllerWrapper.newInstance(mContext);
+ private OtDaemonController mOtCtl;
+ private FullThreadDevice mFtd;
+ private HandlerThread mHandlerThread;
+ private TapTestNetworkTracker mTestNetworkTracker;
+
+ @Before
+ public void setUp() throws Exception {
+ mExecutor = Executors.newSingleThreadExecutor();
+ mFtd = new FullThreadDevice(10 /* nodeId */);
+ mOtCtl = new OtDaemonController();
+ mController.setEnabledAndWait(true);
+ mController.setConfigurationAndWait(
+ new ThreadConfiguration.Builder().setBorderRouterEnabled(true).build());
+ mController.leaveAndWait();
+
+ mHandlerThread = new HandlerThread("ThreadIntegrationTest");
+ mHandlerThread.start();
+
+ mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
+ assertThat(mTestNetworkTracker).isNotNull();
+ mController.setTestNetworkAsUpstreamAndWait(mTestNetworkTracker.getInterfaceName());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ ThreadStateListener.stopAllListeners();
+
+ if (mTestNetworkTracker != null) {
+ mTestNetworkTracker.tearDown();
+ }
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+ }
+ mController.setTestNetworkAsUpstreamAndWait(null);
+ mController.leaveAndWait();
+
+ mFtd.destroy();
+ mExecutor.shutdownNow();
+ }
+
+ @Test
+ public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoinedAndTunIfStateConsistent()
+ throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ runShellCommand("stop ot-daemon");
+
+ mController.waitForRole(DEVICE_ROLE_DETACHED, CALLBACK_TIMEOUT);
+ mController.waitForRole(DEVICE_ROLE_LEADER, RESTART_JOIN_TIMEOUT);
+ assertThat(mOtCtl.isInterfaceUp()).isTrue();
+ assertThat(runShellCommand("ifconfig thread-wpan")).contains("UP POINTOPOINT RUNNING");
+ }
+
+ @Test
+ public void joinNetwork_tunInterfaceJoinsAllRouterMulticastGroup() throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ waitFor(
+ () -> isInMulticastGroup("thread-wpan", GROUP_ADDR_ALL_ROUTERS),
+ TUN_ADDR_UPDATE_TIMEOUT);
+ }
+
+ @Test
+ public void joinNetwork_allMlAddrAreNotPreferredAndOmrIsPreferred() throws Exception {
+ mController.setTestNetworkAsUpstreamAndWait(mTestNetworkTracker.getInterfaceName());
+ mController.joinAndWait(DEFAULT_DATASET);
+ waitFor(
+ () -> getIpv6Addresses("thread-wpan").contains(mOtCtl.getOmrAddress()),
+ OMR_LINK_ADDR_TIMEOUT);
+
+ IpPrefix meshLocalPrefix = DEFAULT_DATASET.getMeshLocalPrefix();
+ 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();
+ }
+
+ @Test
+ @RequiresSimulationThreadDevice
+ public void edPingsMeshLocalAddresses_oneReplyPerRequest() throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+ startFtdChild(mFtd, DEFAULT_DATASET);
+ List<Inet6Address> meshLocalAddresses = mOtCtl.getMeshLocalAddresses();
+
+ for (Inet6Address address : meshLocalAddresses) {
+ assertWithMessage(
+ "There may be duplicated replies of ping request to "
+ + address.getHostAddress())
+ .that(mFtd.ping(address, 2))
+ .isEqualTo(2);
+ }
+ }
+
+ @Test
+ public void addPrefixToNetData_routeIsAddedToTunInterface() throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ // Ftd child doesn't have the ability to add a prefix, so let BR itself add a prefix.
+ mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
+ mOtCtl.executeCommand("netdata register");
+ waitFor(
+ () -> {
+ String netData = mOtCtl.executeCommand("netdata show");
+ return getPrefixesFromNetData(netData).contains(TEST_NO_SLAAC_PREFIX);
+ },
+ NET_DATA_UPDATE_TIMEOUT);
+
+ assertRouteAddedOrRemovedInLinkProperties(true /* isAdded */, TEST_NO_SLAAC_PREFIX_ADDRESS);
+ }
+
+ @Test
+ public void removePrefixFromNetData_routeIsRemovedFromTunInterface() throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+ mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
+ mOtCtl.executeCommand("netdata register");
+
+ mOtCtl.executeCommand("prefix remove " + TEST_NO_SLAAC_PREFIX);
+ mOtCtl.executeCommand("netdata register");
+ waitFor(
+ () -> {
+ String netData = mOtCtl.executeCommand("netdata show");
+ return !getPrefixesFromNetData(netData).contains(TEST_NO_SLAAC_PREFIX);
+ },
+ NET_DATA_UPDATE_TIMEOUT);
+
+ assertRouteAddedOrRemovedInLinkProperties(
+ false /* isAdded */, TEST_NO_SLAAC_PREFIX_ADDRESS);
+ }
+
+ @Test
+ public void toggleThreadNetwork_routeFromPreviousNetDataIsRemoved() throws Exception {
+ mController.joinAndWait(DEFAULT_DATASET);
+ mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
+ mOtCtl.executeCommand("netdata register");
+
+ mController.leaveAndWait();
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ assertRouteAddedOrRemovedInLinkProperties(
+ false /* isAdded */, TEST_NO_SLAAC_PREFIX_ADDRESS);
+ }
+
+ private void startFtdChild(FullThreadDevice ftd, ActiveOperationalDataset activeDataset)
+ throws Exception {
+ ftd.factoryReset();
+ ftd.joinNetwork(activeDataset);
+ ftd.waitForStateAnyOf(List.of("router", "child"), Duration.ofSeconds(8));
+ }
+
+ private void assertRouteAddedOrRemovedInLinkProperties(boolean isAdded, InetAddress addr)
+ throws Exception {
+ ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+
+ waitFor(
+ () -> {
+ try {
+ LinkProperties lp =
+ cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
+ return lp != null
+ && isAdded
+ == lp.getRoutes().stream().anyMatch(r -> r.matches(addr));
+ } catch (Exception e) {
+ return false;
+ }
+ },
+ LINK_PROPERTIES_UPDATE_TIMEOUT);
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/borderrouter/src/android/net/thread/borderrouter/BorderRoutingTest.java
similarity index 99%
rename from thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
rename to thread/tests/integration/borderrouter/src/android/net/thread/borderrouter/BorderRoutingTest.java
index 40f0089..1d210c6 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/borderrouter/src/android/net/thread/borderrouter/BorderRoutingTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.net.thread;
+package android.net.thread.borderrouter;
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
@@ -92,10 +92,10 @@
import java.util.function.Predicate;
/** Integration test cases for Thread Border Routing feature. */
-@RunWith(AndroidJUnit4.class)
+@LargeTest
@RequiresThreadFeature
@RequiresSimulationThreadDevice
-@LargeTest
+@RunWith(AndroidJUnit4.class)
public class BorderRoutingTest {
private static final String TAG = BorderRoutingTest.class.getSimpleName();
private static final int NUM_FTD = 2;
diff --git a/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt b/thread/tests/integration/borderrouter/src/android/net/thread/borderrouter/InternetAccessTest.kt
similarity index 99%
rename from thread/tests/integration/src/android/net/thread/InternetAccessTest.kt
rename to thread/tests/integration/borderrouter/src/android/net/thread/borderrouter/InternetAccessTest.kt
index 46d4708..ad98305 100644
--- a/thread/tests/integration/src/android/net/thread/InternetAccessTest.kt
+++ b/thread/tests/integration/borderrouter/src/android/net/thread/borderrouter/InternetAccessTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.net.thread
+package android.net.thread.borderrouter
import android.content.Context
import android.net.DnsResolver.CLASS_IN
@@ -60,10 +60,10 @@
import org.junit.runner.RunWith
/** Integration test cases for Thread Internet Access features. */
+@LargeTest
@RunWith(AndroidJUnit4::class)
@RequiresThreadFeature
@RequiresSimulationThreadDevice
-@LargeTest
class InternetAccessTest {
companion object {
private val TAG = BorderRoutingTest::class.java.simpleName
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index 5b07e0a..b608c5d 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -21,26 +21,20 @@
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
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;
-import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
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;
@@ -49,23 +43,21 @@
import android.net.ConnectivityManager;
import android.net.InetAddresses;
import android.net.IpPrefix;
-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 androidx.test.runner.AndroidJUnit4;
import com.google.common.collect.FluentIterable;
@@ -74,7 +66,6 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
import java.io.IOException;
import java.net.DatagramPacket;
@@ -83,7 +74,6 @@
import java.net.InetAddress;
import java.time.Duration;
import java.util.Arrays;
-import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
@@ -92,7 +82,7 @@
/** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */
@LargeTest
@RequiresThreadFeature
-@RunWith(Parameterized.class)
+@RunWith(AndroidJUnit4.class)
public class ThreadIntegrationTest {
// The byte[] buffer size for UDP tests
private static final int UDP_BUFFER_SIZE = 1024;
@@ -100,17 +90,8 @@
// The maximum time for OT addresses to be propagated to the TUN interface "thread-wpan"
private static final Duration TUN_ADDR_UPDATE_TIMEOUT = Duration.ofSeconds(1);
- // The maximum time for changes to be propagated to netdata.
- private static final Duration NET_DATA_UPDATE_TIMEOUT = Duration.ofSeconds(1);
-
- // The maximum time for changes in netdata to be propagated to link properties.
- private static final Duration LINK_PROPERTIES_UPDATE_TIMEOUT = Duration.ofSeconds(1);
-
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);
@@ -125,9 +106,6 @@
private static final ActiveOperationalDataset DEFAULT_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
- private static final Inet6Address GROUP_ADDR_ALL_ROUTERS =
- (Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
-
private static final String TEST_NO_SLAAC_PREFIX = "9101:dead:beef:cafe::/64";
private static final InetAddress TEST_NO_SLAAC_PREFIX_ADDRESS =
InetAddresses.parseNumericAddress("9101:dead:beef:cafe::");
@@ -140,54 +118,22 @@
ThreadNetworkControllerWrapper.newInstance(mContext);
private OtDaemonController mOtCtl;
private FullThreadDevice mFtd;
- private HandlerThread mHandlerThread;
- private TapTestNetworkTracker mTestNetworkTracker;
-
- public final boolean mIsBorderRouterEnabled;
- private final ThreadConfiguration mConfig;
-
- @Parameterized.Parameters
- public static Collection configArguments() {
- return Arrays.asList(new Object[][] {{false}, {true}});
- }
-
- public ThreadIntegrationTest(boolean isBorderRouterEnabled) {
- mIsBorderRouterEnabled = isBorderRouterEnabled;
- mConfig =
- new ThreadConfiguration.Builder()
- .setBorderRouterEnabled(isBorderRouterEnabled)
- .build();
- }
@Before
public void setUp() throws Exception {
mExecutor = Executors.newSingleThreadExecutor();
+ mFtd = new FullThreadDevice(10 /* nodeId */);
mOtCtl = new OtDaemonController();
mController.setEnabledAndWait(true);
- mController.setConfigurationAndWait(mConfig);
+ mController.setConfigurationAndWait(
+ new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build());
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 */);
}
@After
public void tearDown() throws Exception {
ThreadStateListener.stopAllListeners();
- if (mTestNetworkTracker != null) {
- mTestNetworkTracker.tearDown();
- }
- if (mHandlerThread != null) {
- mHandlerThread.quitSafely();
- mHandlerThread.join();
- }
mController.setTestNetworkAsUpstreamAndWait(null);
mController.leaveAndWait();
@@ -207,20 +153,6 @@
}
@Test
- public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoinedAndTunIfStateConsistent()
- throws Exception {
- assumeTrue(mController.getConfiguration().isBorderRouterEnabled());
- mController.joinAndWait(DEFAULT_DATASET);
-
- runShellCommand("stop ot-daemon");
-
- mController.waitForRole(DEVICE_ROLE_DETACHED, CALLBACK_TIMEOUT);
- mController.waitForRole(DEVICE_ROLE_LEADER, RESTART_JOIN_TIMEOUT);
- assertThat(mOtCtl.isInterfaceUp()).isTrue();
- assertThat(runShellCommand("ifconfig thread-wpan")).contains("UP POINTOPOINT RUNNING");
- }
-
- @Test
public void otDaemonFactoryReset_deviceRoleIsStopped() throws Exception {
mController.joinAndWait(DEFAULT_DATASET);
@@ -294,33 +226,7 @@
}
@Test
- 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();
- 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();
- }
-
- @Test
- public void joinNetwork_borderRouterDisabled_onlyMlEidIsPreferred() throws Exception {
- assumeFalse(mConfig.isBorderRouterEnabled());
-
+ public void joinNetwork_onlyMlEidIsPreferred() throws Exception {
mController.joinAndWait(DEFAULT_DATASET);
waitFor(
() -> getIpv6Addresses("thread-wpan").contains(mOtCtl.getMlEid()),
@@ -342,13 +248,6 @@
}
@Test
- public void joinNetwork_tunInterfaceJoinsAllRouterMulticastGroup() throws Exception {
- mController.joinAndWait(DEFAULT_DATASET);
-
- assertTunInterfaceMemberOfGroup(GROUP_ADDR_ALL_ROUTERS);
- }
-
- @Test
public void joinNetwork_joinTheSameNetworkTwice_neverDetached() throws Exception {
mController.joinAndWait(DEFAULT_DATASET);
mController.waitForRole(DEVICE_ROLE_LEADER, JOIN_TIMEOUT);
@@ -379,55 +278,6 @@
}
@Test
- public void addPrefixToNetData_routeIsAddedToTunInterface() throws Exception {
- mController.joinAndWait(DEFAULT_DATASET);
-
- // Ftd child doesn't have the ability to add a prefix, so let BR itself add a prefix.
- mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
- mOtCtl.executeCommand("netdata register");
- waitFor(
- () -> {
- String netData = mOtCtl.executeCommand("netdata show");
- return getPrefixesFromNetData(netData).contains(TEST_NO_SLAAC_PREFIX);
- },
- NET_DATA_UPDATE_TIMEOUT);
-
- assertRouteAddedOrRemovedInLinkProperties(true /* isAdded */, TEST_NO_SLAAC_PREFIX_ADDRESS);
- }
-
- @Test
- public void removePrefixFromNetData_routeIsRemovedFromTunInterface() throws Exception {
- mController.joinAndWait(DEFAULT_DATASET);
- mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
- mOtCtl.executeCommand("netdata register");
-
- mOtCtl.executeCommand("prefix remove " + TEST_NO_SLAAC_PREFIX);
- mOtCtl.executeCommand("netdata register");
- waitFor(
- () -> {
- String netData = mOtCtl.executeCommand("netdata show");
- return !getPrefixesFromNetData(netData).contains(TEST_NO_SLAAC_PREFIX);
- },
- NET_DATA_UPDATE_TIMEOUT);
-
- assertRouteAddedOrRemovedInLinkProperties(
- false /* isAdded */, TEST_NO_SLAAC_PREFIX_ADDRESS);
- }
-
- @Test
- public void toggleThreadNetwork_routeFromPreviousNetDataIsRemoved() throws Exception {
- mController.joinAndWait(DEFAULT_DATASET);
- mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
- mOtCtl.executeCommand("netdata register");
-
- mController.leaveAndWait();
- mController.joinAndWait(DEFAULT_DATASET);
-
- assertRouteAddedOrRemovedInLinkProperties(
- false /* isAdded */, TEST_NO_SLAAC_PREFIX_ADDRESS);
- }
-
- @Test
@RequiresSimulationThreadDevice
public void setConfiguration_disableBorderRouter_borderRoutingDisabled() throws Exception {
startFtdLeader(mFtd, DEFAULT_DATASET);
@@ -502,27 +352,4 @@
throw new IllegalStateException(e);
}
}
-
- private void assertTunInterfaceMemberOfGroup(Inet6Address address) throws Exception {
- waitFor(() -> isInMulticastGroup(TUN_IF_NAME, address), TUN_ADDR_UPDATE_TIMEOUT);
- }
-
- private void assertRouteAddedOrRemovedInLinkProperties(boolean isAdded, InetAddress addr)
- throws Exception {
- ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
-
- waitFor(
- () -> {
- try {
- LinkProperties lp =
- cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
- return lp != null
- && isAdded
- == lp.getRoutes().stream().anyMatch(r -> r.matches(addr));
- } catch (Exception e) {
- return false;
- }
- },
- LINK_PROPERTIES_UPDATE_TIMEOUT);
- }
}
diff --git a/thread/tests/utils/Android.bp b/thread/tests/utils/Android.bp
index 726ec9d..7990752 100644
--- a/thread/tests/utils/Android.bp
+++ b/thread/tests/utils/Android.bp
@@ -31,6 +31,7 @@
],
srcs: [
"src/**/*.java",
+ "src/**/*.kt",
],
defaults: [
"framework-connectivity-test-defaults",
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/utils/src/android/net/thread/utils/FullThreadDevice.java
similarity index 100%
rename from thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
rename to thread/tests/utils/src/android/net/thread/utils/FullThreadDevice.java
diff --git a/thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java b/thread/tests/utils/src/android/net/thread/utils/InfraNetworkDevice.java
similarity index 100%
rename from thread/tests/integration/src/android/net/thread/utils/InfraNetworkDevice.java
rename to thread/tests/utils/src/android/net/thread/utils/InfraNetworkDevice.java
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/utils/src/android/net/thread/utils/IntegrationTestUtils.kt
similarity index 99%
rename from thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
rename to thread/tests/utils/src/android/net/thread/utils/IntegrationTestUtils.kt
index f7b4d19..77d0955 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/utils/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -455,9 +455,8 @@
fun isInMulticastGroup(interfaceName: String, address: Inet6Address): Boolean {
val cmd = "ip -6 maddr show dev $interfaceName"
val output: String = runShellCommandOrThrow(cmd)
- val addressStr = address.hostAddress
for (line in output.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
- if (line.contains(addressStr)) {
+ if (line.contains(address.hostAddress!!)) {
return true
}
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java b/thread/tests/utils/src/android/net/thread/utils/OtDaemonController.java
similarity index 100%
rename from thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
rename to thread/tests/utils/src/android/net/thread/utils/OtDaemonController.java
diff --git a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
index b586a19..9a1a05b 100644
--- a/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
+++ b/thread/tests/utils/src/android/net/thread/utils/TapTestNetworkTracker.java
@@ -100,27 +100,17 @@
private void setUpTestNetwork() throws Exception {
mInterface = mContext.getSystemService(TestNetworkManager.class).createTapInterface();
- mConnectivityManager.requestNetwork(newNetworkRequest(), mNetworkCallback);
+ mConnectivityManager.requestNetwork(
+ TestableNetworkAgent.Companion.makeNetworkRequestForInterface(
+ mInterface.getInterfaceName()),
+ mNetworkCallback);
LinkProperties lp = new LinkProperties();
lp.setInterfaceName(getInterfaceName());
mAgent =
- new TestableNetworkAgent(
- mContext,
- mLooper,
- newNetworkCapabilities(),
- lp,
- new NetworkAgentConfig.Builder().build());
- mNetwork = mAgent.register();
- mAgent.markConnected();
+ TestableNetworkAgent.Companion.createOnInterface(
+ mContext, mLooper, mInterface.getInterfaceName(), TIMEOUT.toMillis());
- PollingCheck.check(
- "No usable address on interface",
- TIMEOUT.toMillis(),
- () -> hasUsableAddress(mNetwork, getInterfaceName()));
-
- lp.setLinkAddresses(makeLinkAddresses());
- mAgent.sendLinkProperties(lp);
mNetworkCallback.eventuallyExpect(
LINK_PROPERTIES_CHANGED,
TIMEOUT.toMillis(),
@@ -133,59 +123,4 @@
mInterface.getFileDescriptor().close();
mAgent.waitForIdle(TIMEOUT.toMillis());
}
-
- private NetworkRequest newNetworkRequest() {
- return new NetworkRequest.Builder()
- .removeCapability(NET_CAPABILITY_TRUSTED)
- .addTransportType(TRANSPORT_TEST)
- .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()))
- .build();
- }
-
- private NetworkCapabilities newNetworkCapabilities() {
- return new NetworkCapabilities()
- .removeCapability(NET_CAPABILITY_TRUSTED)
- .addTransportType(TRANSPORT_TEST)
- .setNetworkSpecifier(new TestNetworkSpecifier(getInterfaceName()));
- }
-
- private List<LinkAddress> makeLinkAddresses() {
- List<LinkAddress> linkAddresses = new ArrayList<>();
- List<InterfaceAddress> interfaceAddresses = Collections.emptyList();
-
- try {
- interfaceAddresses =
- NetworkInterface.getByName(getInterfaceName()).getInterfaceAddresses();
- } catch (SocketException ignored) {
- // Ignore failures when getting the addresses.
- }
-
- for (InterfaceAddress address : interfaceAddresses) {
- linkAddresses.add(
- new LinkAddress(address.getAddress(), address.getNetworkPrefixLength()));
- }
-
- return linkAddresses;
- }
-
- private static boolean hasUsableAddress(Network network, String interfaceName) {
- try {
- if (NetworkInterface.getByName(interfaceName).getInterfaceAddresses().isEmpty()) {
- return false;
- }
- } catch (SocketException e) {
- return false;
- }
- // Check if the link-local address can be used. Address flags are not available without
- // elevated permissions, so check that bindSocket works.
- try {
- FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
- network.bindSocket(sock);
- Os.connect(sock, parseNumericAddress("ff02::fb%" + interfaceName), 12345);
- Os.close(sock);
- } catch (ErrnoException | IOException e) {
- return false;
- }
- return true;
- }
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/TestDnsServer.kt b/thread/tests/utils/src/android/net/thread/utils/TestDnsServer.kt
similarity index 100%
rename from thread/tests/integration/src/android/net/thread/utils/TestDnsServer.kt
rename to thread/tests/utils/src/android/net/thread/utils/TestDnsServer.kt
diff --git a/thread/tests/integration/src/android/net/thread/utils/TestTunNetworkUtils.kt b/thread/tests/utils/src/android/net/thread/utils/TestTunNetworkUtils.kt
similarity index 100%
rename from thread/tests/integration/src/android/net/thread/utils/TestTunNetworkUtils.kt
rename to thread/tests/utils/src/android/net/thread/utils/TestTunNetworkUtils.kt
diff --git a/thread/tests/integration/src/android/net/thread/utils/TestUdpEchoServer.kt b/thread/tests/utils/src/android/net/thread/utils/TestUdpEchoServer.kt
similarity index 100%
rename from thread/tests/integration/src/android/net/thread/utils/TestUdpEchoServer.kt
rename to thread/tests/utils/src/android/net/thread/utils/TestUdpEchoServer.kt
diff --git a/thread/tests/integration/src/android/net/thread/utils/TestUdpServer.kt b/thread/tests/utils/src/android/net/thread/utils/TestUdpServer.kt
similarity index 100%
rename from thread/tests/integration/src/android/net/thread/utils/TestUdpServer.kt
rename to thread/tests/utils/src/android/net/thread/utils/TestUdpServer.kt
diff --git a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java b/thread/tests/utils/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
similarity index 100%
rename from thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
rename to thread/tests/utils/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java