Merge "Add CachedService Class" into main
diff --git a/OWNERS_core_networking_xts b/OWNERS_core_networking_xts
index 9e4e4a1..60ca885 100644
--- a/OWNERS_core_networking_xts
+++ b/OWNERS_core_networking_xts
@@ -10,3 +10,5 @@
# In addition to cherry-picks, flaky test fixes and no-op refactors, also for
# NsdManager tests
reminv@google.com #{LAST_RESORT_SUGGESTION}
+# Only for APF firmware tests (to verify correct behaviour of the wifi APF interpreter)
+yuyanghuang@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 2878f79..531489d 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -47,15 +47,6 @@
// as the above target may have different "enabled" values
// depending on the branch
-apex_defaults {
- name: "CronetInTetheringApexDefaults",
- jni_libs: [
- "cronet_aml_components_cronet_android_cronet",
- "//external/cronet/third_party/boringssl:libcrypto",
- "//external/cronet/third_party/boringssl:libssl",
- ],
-}
-
apex {
name: "com.android.tethering",
defaults: [
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index e2498e4..d2a8c13 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -111,6 +111,7 @@
"sdk_module-lib_current_framework-wifi",
],
static_libs: [
+ "modules-utils-build",
"com.android.net.flags-aconfig-java",
],
aidl: {
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 0ac97f0..2f9c3bc 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -37,11 +37,13 @@
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;
import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.flags.Flags;
import java.lang.annotation.Retention;
@@ -664,7 +666,7 @@
}
private void unsupportedAfterV() {
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ if (SdkLevel.isAtLeastB()) {
throw new UnsupportedOperationException("Not supported after SDK version "
+ Build.VERSION_CODES.VANILLA_ICE_CREAM);
}
@@ -1257,6 +1259,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
*/
@@ -1278,6 +1318,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..9c61716 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,83 @@
*
* @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);
+ }
+
+ /**
+ * 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 1589509..c7ae353 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -43,6 +43,7 @@
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
import static android.net.TetheringManager.TETHERING_WIGIG;
import static android.net.TetheringManager.TETHER_ERROR_BLUETOOTH_SERVICE_PENDING;
+import static android.net.TetheringManager.TETHER_ERROR_DUPLICATE_REQUEST;
import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
@@ -67,6 +68,8 @@
import static android.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;
@@ -117,7 +120,6 @@
import android.net.wifi.p2p.WifiP2pInfo;
import android.net.wifi.p2p.WifiP2pManager;
import android.os.Binder;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -290,7 +292,10 @@
private SettingsObserver mSettingsObserver;
private BluetoothPan mBluetoothPan;
private PanServiceListener mBluetoothPanListener;
- private final ArrayList<IIntResultListener> mPendingPanRequestListeners;
+ // Pending listener for starting Bluetooth tethering before the PAN service is connected. Once
+ // the service is connected, the bluetooth iface will be requested and the listener will be
+ // called.
+ private IIntResultListener mPendingPanRequestListener;
// AIDL doesn't support Set<Integer>. Maintain a int bitmap here. When the bitmap is passed to
// TetheringManager, TetheringManager would convert it to a set of Integer types.
// mSupportedTypeBitmap should always be updated inside tethering internal thread but it may be
@@ -306,12 +311,7 @@
mLooper = mDeps.makeTetheringLooper();
mNotificationUpdater = mDeps.makeNotificationUpdater(mContext, mLooper);
mTetheringMetrics = mDeps.makeTetheringMetrics(mContext);
- mRequestTracker = new RequestTracker();
-
- // This is intended to ensrure that if something calls startTethering(bluetooth) just after
- // bluetooth is enabled. Before onServiceConnected is called, store the calls into this
- // list and handle them as soon as onServiceConnected is called.
- mPendingPanRequestListeners = new ArrayList<>();
+ mRequestTracker = new RequestTracker(isTetheringWithSoftApConfigEnabled());
mTetherStates = new ArrayMap<>();
mConnectedClientsTracker = new ConnectedClientsTracker();
@@ -637,7 +637,7 @@
// TODO: fix the teardown path to stop depending on interface state notifications.
// These are not necessary since most/all link layers have their own teardown
// notifications, and can race with those notifications.
- if (enabled && Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ if (enabled && SdkLevel.isAtLeastB()) {
return;
}
@@ -706,9 +706,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()) {
@@ -728,39 +735,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.
@@ -804,21 +800,25 @@
// The result of Bluetooth tethering will be sent after the pan service connects.
if (result == TETHER_ERROR_BLUETOOTH_SERVICE_PENDING) return;
- sendTetherResult(listener, result, type);
+ sendTetherResultAndRemoveOnError(request, listener, result);
}
- private void sendTetherResult(final IIntResultListener listener, final int result,
- final int type) {
+ private void sendTetherResult(final IIntResultListener listener, final int result) {
if (listener != null) {
try {
listener.onResult(result);
- } catch (RemoteException e) { }
+ } catch (RemoteException e) {
+ }
}
+ }
- // 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);
}
@@ -862,15 +862,20 @@
if (!enable) {
// The service is not connected. If disabling tethering, there's no point starting
// the service just to stop tethering since tethering is not started. Just remove
- // any pending requests to enable tethering, and notify them that they have failed.
- for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
- sendTetherResult(pendingListener, TETHER_ERROR_SERVICE_UNAVAIL,
- TETHERING_BLUETOOTH);
+ // any pending request to enable tethering, and notify them that they have failed.
+ if (mPendingPanRequestListener != null) {
+ sendTetherResult(mPendingPanRequestListener, TETHER_ERROR_SERVICE_UNAVAIL);
}
- mPendingPanRequestListeners.clear();
+ mPendingPanRequestListener = null;
return TETHER_ERROR_NO_ERROR;
}
- mPendingPanRequestListeners.add(listener);
+
+ // Only allow one pending request at a time.
+ if (mPendingPanRequestListener != null) {
+ return TETHER_ERROR_SERVICE_UNAVAIL;
+ }
+
+ mPendingPanRequestListener = listener;
// Bluetooth tethering is not a popular feature. To avoid bind to bluetooth pan service all
// the time but user never use bluetooth tethering. mBluetoothPanListener is created first
@@ -897,12 +902,15 @@
mBluetoothPan = (BluetoothPan) proxy;
mIsConnected = true;
- for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+ if (mPendingPanRequestListener != null) {
final int result = setBluetoothTetheringSettings(mBluetoothPan,
true /* enable */);
- sendTetherResult(pendingListener, result, TETHERING_BLUETOOTH);
+ sendTetherResultAndRemoveOnError(
+ mRequestTracker.getOrCreatePendingRequest(TETHERING_BLUETOOTH),
+ mPendingPanRequestListener,
+ result);
}
- mPendingPanRequestListeners.clear();
+ mPendingPanRequestListener = null;
});
}
@@ -913,11 +921,13 @@
// reachable before next onServiceConnected.
mIsConnected = false;
- for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
- sendTetherResult(pendingListener, TETHER_ERROR_SERVICE_UNAVAIL,
- TETHERING_BLUETOOTH);
+ if (mPendingPanRequestListener != null) {
+ sendTetherResultAndRemoveOnError(
+ mRequestTracker.getOrCreatePendingRequest(TETHERING_BLUETOOTH),
+ mPendingPanRequestListener,
+ TETHER_ERROR_SERVICE_UNAVAIL);
}
- mPendingPanRequestListeners.clear();
+ mPendingPanRequestListener = null;
mBluetoothIfaceRequest = null;
mBluetoothCallback = null;
maybeDisableBluetoothIpServing();
@@ -1078,7 +1088,7 @@
}
private void handleLegacyTether(String iface, final IIntResultListener listener) {
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ if (SdkLevel.isAtLeastB()) {
// After V, the TetheringManager and ConnectivityManager tether and untether methods
// throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
// that this code cannot run even if callers use raw binder calls or other
@@ -1089,9 +1099,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);
@@ -1128,9 +1136,7 @@
// Do nothing
break;
}
- try {
- listener.onResult(result);
- } catch (RemoteException e) { }
+ sendTetherResult(listener, result);
}
/**
@@ -1163,11 +1169,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,
@@ -1179,7 +1183,7 @@
}
void legacyUntether(String iface, final IIntResultListener listener) {
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ if (SdkLevel.isAtLeastB()) {
// After V, the TetheringManager and ConnectivityManager tether and untether methods
// throw UnsupportedOperationException, so this cannot happen in normal use. Ensure
// that this code cannot run even if callers use raw binder calls or other
@@ -1187,10 +1191,7 @@
return;
}
mHandler.post(() -> {
- try {
- listener.onResult(legacyUntetherInternal(iface));
- } catch (RemoteException e) {
- }
+ sendTetherResult(listener, legacyUntetherInternal(iface));
});
}
@@ -1205,7 +1206,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;
}
@@ -1285,7 +1286,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,
@@ -1607,7 +1613,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;
}
@@ -1624,7 +1630,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;
}
}
@@ -1771,9 +1777,7 @@
void setUsbTethering(boolean enable, IIntResultListener listener) {
mHandler.post(() -> {
- try {
- listener.onResult(setUsbTethering(enable));
- } catch (RemoteException e) { }
+ sendTetherResult(listener, setUsbTethering(enable));
});
}
@@ -2900,6 +2904,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);
}
@@ -2914,7 +2921,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:
@@ -3010,11 +3029,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);
}
@@ -3022,9 +3048,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 8e17085..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,10 +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 Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM
- && Flags.tetheringWithSoftApConfig();
+ return SdkLevel.isAtLeastB();
}
}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index b553f46..f501a50 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -173,7 +173,21 @@
@Override
public void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver,
boolean showEntitlementUi, String callerPkg, String callingAttributionTag) {
- if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, receiver)) return;
+ // Wrap the app-provided ResultReceiver in an IIntResultListener in order to call
+ // checkAndNotifyCommonError with it.
+ IIntResultListener listener = new IIntResultListener() {
+ @Override
+ public void onResult(int i) {
+ receiver.send(i, null);
+ }
+
+ @Override
+ public IBinder asBinder() {
+ throw new UnsupportedOperationException("asBinder unexpectedly called on"
+ + " internal-only listener");
+ }
+ };
+ if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return;
mTethering.requestLatestTetheringEntitlementResult(type, receiver, showEntitlementUi);
}
@@ -277,27 +291,6 @@
return false;
}
- private boolean checkAndNotifyCommonError(final String callerPkg,
- final String callingAttributionTag, final ResultReceiver receiver) {
- if (!checkPackageNameMatchesUid(getBinderCallingUid(), callerPkg)) {
- Log.e(TAG, "Package name " + callerPkg + " does not match UID "
- + getBinderCallingUid());
- receiver.send(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION, null);
- return true;
- }
- if (!hasTetherChangePermission(callerPkg, callingAttributionTag,
- false /* onlyAllowPrivileged */)) {
- receiver.send(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION, null);
- return true;
- }
- if (!mTethering.isTetheringSupported() || !mTethering.isTetheringAllowed()) {
- receiver.send(TETHER_ERROR_UNSUPPORTED, null);
- return true;
- }
-
- return false;
- }
-
private boolean hasNetworkSettingsPermission() {
return checkCallingOrSelfPermission(NETWORK_SETTINGS);
}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index ee82776..c282618 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -51,11 +51,13 @@
"src/**/*.kt",
],
static_libs: [
+ // Include mockito extended first so it takes precedence, as other libraries like
+ // TetheringCommonTests bundle non-extended mockito.
+ // TODO: use non-extended mockito in tethering tests instead
+ "mockito-target-extended-minus-junit4",
"TetheringCommonTests",
"androidx.test.rules",
"frameworks-base-testutils",
- "mockito-target-extended-minus-junit4",
- "net-tests-utils",
"testables",
"truth",
],
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 51efaf8..1083ef9 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;
@@ -101,6 +104,7 @@
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
@@ -307,8 +311,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();
@@ -339,6 +342,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) {
@@ -395,6 +399,9 @@
}
public class MockIpServerDependencies extends IpServer.Dependencies {
+
+ private int mOnDhcpServerCreatedResult = STATUS_SUCCESS;
+
@Override
public DadProxy getDadProxy(
Handler handler, InterfaceParams ifParams) {
@@ -436,7 +443,7 @@
DhcpServerCallbacks cb) {
new Thread(() -> {
try {
- cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer);
+ cb.onDhcpServerCreated(mOnDhcpServerCreatedResult, mDhcpServer);
} catch (RemoteException e) {
fail(e.getMessage());
}
@@ -447,6 +454,10 @@
IpNeighborMonitor.NeighborEventConsumer c) {
return mIpNeighborMonitor;
}
+
+ public void setOnDhcpServerCreatedResult(final int result) {
+ mOnDhcpServerCreatedResult = result;
+ }
}
public class MockTetheringDependencies extends TetheringDependencies {
@@ -572,6 +583,11 @@
public int getBinderCallingUid() {
return mBinderCallingUid;
}
+
+ @Override
+ public boolean isTetheringWithSoftApConfigEnabled() {
+ return mTetheringWithSoftApConfigEnabled;
+ }
}
private static LinkProperties buildUpstreamLinkProperties(String interfaceName,
@@ -707,6 +723,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.
@@ -1943,7 +1962,6 @@
@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,
@@ -1980,7 +1998,6 @@
@Test
public void workingWifiTetheringEnrichedApBroadcast() 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,
@@ -2029,7 +2046,6 @@
@Test
public void failureEnablingIpForwarding() throws Exception {
initTetheringOnTestThread();
- when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
// Emulate pressing the WiFi tethering button.
@@ -2079,9 +2095,9 @@
TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR);
verify(mTetheringMetrics, times(0)).maybeUpdateUpstreamType(any());
- verify(mTetheringMetrics, times(2)).updateErrorCode(eq(TETHERING_WIFI),
+ verify(mTetheringMetrics, times(1)).updateErrorCode(eq(TETHERING_WIFI),
eq(TETHER_ERROR_INTERNAL_ERROR));
- verify(mTetheringMetrics, times(2)).sendReport(eq(TETHERING_WIFI));
+ verify(mTetheringMetrics, times(1)).sendReport(eq(TETHERING_WIFI));
verifyNoMoreInteractions(mWifiManager);
verifyNoMoreInteractions(mNetd);
@@ -2373,7 +2389,6 @@
// 2. Enable wifi tethering.
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
initTetheringUpstream(upstreamState);
- when(mWifiManager.startTetheredHotspot(null)).thenReturn(true);
mTethering.startTethering(createTetheringRequest(TETHERING_WIFI), TEST_CALLER_PKG,
null);
@@ -2424,7 +2439,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))
@@ -2476,19 +2491,16 @@
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();
- }
+ // Netd "up" event should not trigger a state change callback in B+.
+ callback.assertNoStateChangeCallback();
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
- mLooper.dispatchAll();
// Verify we see Available -> Tethered states
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
callback.pollTetherStatesChanged().availableList);
@@ -2506,20 +2518,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,
+ mLooper.dispatchAll();
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME,
IFACE_IP_MODE_TETHERED);
- }
- assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
callback.pollTetherStatesChanged().availableList);
assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
differentCallback.pollTetherStatesChanged().availableList);
- assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
settingsCallback.pollTetherStatesChanged().availableList);
mLooper.dispatchAll();
callback.expectUpstreamChanged(NULL_NETWORK);
@@ -2528,6 +2535,320 @@
}
@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.assertHasResult();
+
+ // Try starting wifi tethering with various fuzzy-matching requests and verify we get
+ // TETHER_ERROR_DUPLICATE_REQUEST.
+
+ // Different static IP addresses
+ final TetheringRequest differentIpAddr = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(softApConfig)
+ .setStaticIpv4Addresses(new LinkAddress("192.168.0.123/24"),
+ new LinkAddress("192.168.0.42/24"))
+ .build();
+ differentIpAddr.setUid(TEST_CALLER_UID);
+ ResultListener differentIpAddrListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
+ mTethering.startTethering(differentIpAddr, TEST_CALLER_PKG, differentIpAddrListener);
+ mLooper.dispatchAll();
+ verify(mWifiManager, times(1)).startTetheredHotspot(any());
+ verify(mWifiManager, never()).stopSoftAp();
+ differentIpAddrListener.assertHasResult();
+
+ // Different UID
+ final TetheringRequest differentUid = new TetheringRequest.Builder(TETHERING_WIFI)
+ .setSoftApConfiguration(softApConfig).build();
+ differentUid.setUid(TEST_CALLER_UID + 1);
+ ResultListener differentUidListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
+ mTethering.startTethering(differentUid, TEST_CALLER_PKG, differentUidListener);
+ mLooper.dispatchAll();
+ differentUidListener.assertHasResult();
+ verify(mWifiManager, times(1)).startTetheredHotspot(any());
+ verify(mWifiManager, never()).stopSoftAp();
+
+ // Mock the link layer event to start IP serving and verify we still get
+ // TETHER_ERROR_DUPLICATE_REQUEST even though the request is no longer pending and is
+ // already serving.
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithoutConfig},
+ callback.pollTetherStatesChanged().availableList);
+ assertArrayEquals(new TetheringInterface[] {wifiIfaceWithConfig},
+ callback.pollTetherStatesChanged().tetheredList);
+ callback.expectUpstreamChanged(upstreamState.network);
+ callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STARTED);
+ differentIpAddrListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
+ differentUidListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
+ mTethering.startTethering(differentIpAddr, TEST_CALLER_PKG, differentIpAddrListener);
+ mTethering.startTethering(differentUid, TEST_CALLER_PKG, differentUidListener);
+ mLooper.dispatchAll();
+ differentIpAddrListener.assertHasResult();
+ differentUidListener.assertHasResult();
+ verify(mWifiManager, times(1)).startTetheredHotspot(any());
+ verify(mWifiManager, never()).stopSoftAp();
+ }
+
+ @Test
+ public void testFuzzyMatchedWifiCanBeAddedAfterIpServerStopped() throws Exception {
+ assumeTrue(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();
+ successListener.assertHasResult();
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+
+ // Starting wifi again will cause TETHER_ERROR_DUPLICATE_REQUEST
+ ResultListener failureListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, failureListener);
+ mLooper.dispatchAll();
+ failureListener.assertHasResult();
+
+ // Trigger Netd callback to stop the IpServer
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, false);
+
+ // We should be able to request the same Wifi again
+ ResultListener successListener2 = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener2);
+ mLooper.dispatchAll();
+ successListener2.assertHasResult();
+ }
+
+ @Test
+ public void testFuzzyMatchedWifiCanBeAddedAfterIpServerUnwanted() throws Exception {
+ assumeTrue(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();
+ successListener.assertHasResult();
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+
+ // Starting wifi again will cause TETHER_ERROR_DUPLICATE_REQUEST
+ ResultListener failureListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, failureListener);
+ mLooper.dispatchAll();
+ failureListener.assertHasResult();
+
+ // Trigger wifi ap state change to tell IpServer it's unwanted.
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+
+ // We should be able to request the same Wifi again
+ ResultListener successListener2 = new ResultListener(TETHER_ERROR_NO_ERROR);
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener2);
+ mLooper.dispatchAll();
+ successListener2.assertHasResult();
+ }
+
+ @Test
+ public void testFuzzyMatchedWifiCanBeAddedAfterIpServerError() throws Exception {
+ assumeTrue(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();
+ successListener.assertHasResult();
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+
+ // We should be able to request the same Wifi again since the DHCP server transitioned the
+ // IpServer back to InitialState.
+ ResultListener successListener2 = new ResultListener(TETHER_ERROR_NO_ERROR);
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener2);
+ mLooper.dispatchAll();
+ successListener2.assertHasResult();
+ }
+
+ @Test
+ public void testFuzzyMatchedWifiCanBeAddedAfterStoppingPendingRequest() throws Exception {
+ assumeTrue(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();
+ successListener.assertHasResult();
+
+ // Starting wifi again will cause TETHER_ERROR_DUPLICATE_REQUEST
+ ResultListener failureListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, failureListener);
+ mLooper.dispatchAll();
+ failureListener.assertHasResult();
+
+ // Stop Wifi tethering.
+ mTethering.stopTethering(TETHERING_WIFI);
+
+ // We should be able to request the same Wifi again
+ ResultListener successListener2 = new ResultListener(TETHER_ERROR_NO_ERROR);
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener2);
+ mLooper.dispatchAll();
+ successListener2.assertHasResult();
+ }
+
+ @Test
+ public void testFuzzyMatchedWifiCanBeAddedAfterStoppingServingRequest() throws Exception {
+ assumeTrue(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();
+ successListener.assertHasResult();
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
+
+ // Starting wifi again will cause TETHER_ERROR_DUPLICATE_REQUEST
+ ResultListener failureListener = new ResultListener(TETHER_ERROR_DUPLICATE_REQUEST);
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, failureListener);
+ mLooper.dispatchAll();
+ failureListener.assertHasResult();
+
+ // Stop Wifi tethering.
+ mTethering.stopTethering(TETHERING_WIFI);
+
+ // We should be able to request the same Wifi again
+ ResultListener successListener2 = new ResultListener(TETHER_ERROR_NO_ERROR);
+ mTethering.startTethering(tetheringRequest, TEST_CALLER_PKG, successListener2);
+ mLooper.dispatchAll();
+ successListener2.assertHasResult();
+ }
+
+ @Test
+ public void testStopTetheringWithMatchingRequest() throws Exception {
+ assumeTrue(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();
@@ -2793,7 +3114,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");
@@ -2890,7 +3212,6 @@
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);
@@ -3054,26 +3375,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() {
@@ -3571,6 +3904,33 @@
failedEnable.assertHasResult();
}
+ @Test
+ public void testStartBluetoothTetheringFailsWhenTheresAnExistingRequestWaitingForPanService()
+ throws Exception {
+ mTetheringWithSoftApConfigEnabled = false;
+ initTetheringOnTestThread();
+
+ mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
+ final ResultListener firstResult = new ResultListener(TETHER_ERROR_NO_ERROR);
+ mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
+ TEST_CALLER_PKG, firstResult);
+ mLooper.dispatchAll();
+ firstResult.assertDoesNotHaveResult();
+
+ // Second request should fail.
+ final ResultListener secondResult = new ResultListener(TETHER_ERROR_SERVICE_UNAVAIL);
+ mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
+ TEST_CALLER_PKG, secondResult);
+ mLooper.dispatchAll();
+ secondResult.assertHasResult();
+ firstResult.assertDoesNotHaveResult();
+
+ // Bind to PAN service should succeed for first listener only. If the second result is
+ // called with TETHER_ERROR_NO_ERROR, ResultListener will fail an assertion.
+ verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
+ firstResult.assertHasResult();
+ }
+
private void mockBluetoothSettings(boolean bluetoothOn, boolean tetheringOn) {
when(mBluetoothAdapter.isEnabled()).thenReturn(bluetoothOn);
when(mBluetoothPan.isTetheringOn()).thenReturn(tetheringOn);
@@ -3618,7 +3978,7 @@
private ServiceListener verifySetBluetoothTethering(final boolean enable,
final boolean bindToPanService) throws Exception {
ServiceListener listener = null;
- verify(mBluetoothAdapter).isEnabled();
+ verify(mBluetoothAdapter, atLeastOnce()).isEnabled();
if (bindToPanService) {
final ArgumentCaptor<ServiceListener> listenerCaptor =
ArgumentCaptor.forClass(ServiceListener.class);
@@ -3846,6 +4206,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.
@@ -3858,7 +4233,6 @@
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);
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 5d99b74..3d7ea69 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -1242,6 +1242,22 @@
@ConnectivityManagerFeature
private Long mEnabledConnectivityManagerFeatures = null;
+ /**
+ * A class to help with mocking ConnectivityManager.
+ * @hide
+ */
+ public static class MockHelpers {
+ /**
+ * Produce an instance of the class returned by
+ * {@link ConnectivityManager#registerNetworkAgent}
+ * @hide
+ */
+ public static Network registerNetworkAgentResult(
+ @Nullable final Network network, @Nullable final INetworkAgentRegistry registry) {
+ return network;
+ }
+ }
+
private TetheringManager getTetheringManager() {
synchronized (mTetheringEventCallbacks) {
if (mTetheringManager == null) {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index fb42c03..41b58fa 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -33,8 +33,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Collection;
import java.util.Optional;
/** Helper class to download certificate transparency log files. */
@@ -48,28 +47,21 @@
private final DownloadHelper mDownloadHelper;
private final SignatureVerifier mSignatureVerifier;
private final CertificateTransparencyLogger mLogger;
-
- private final List<CompatibilityVersion> mCompatVersions = new ArrayList<>();
+ private final Collection<CompatibilityVersion> mCompatVersions;
CertificateTransparencyDownloader(
Context context,
DataStore dataStore,
DownloadHelper downloadHelper,
SignatureVerifier signatureVerifier,
- CertificateTransparencyLogger logger) {
+ CertificateTransparencyLogger logger,
+ Collection<CompatibilityVersion> compatVersions) {
mContext = context;
mSignatureVerifier = signatureVerifier;
mDataStore = dataStore;
mDownloadHelper = downloadHelper;
mLogger = logger;
- }
-
- void addCompatibilityVersion(CompatibilityVersion compatVersion) {
- mCompatVersions.add(compatVersion);
- }
-
- void clearCompatibilityVersions() {
- mCompatVersions.clear();
+ mCompatVersions = compatVersions;
}
long startPublicKeyDownload() {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
index f1b9a4f..286f326 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -28,6 +28,8 @@
import android.os.SystemClock;
import android.util.Log;
+import java.util.Collection;
+
/** Implementation of the Certificate Transparency job */
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
public class CertificateTransparencyJob extends BroadcastReceiver {
@@ -37,8 +39,8 @@
private final Context mContext;
private final DataStore mDataStore;
private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
- private final CompatibilityVersion mCompatVersion;
private final SignatureVerifier mSignatureVerifier;
+ private final Collection<CompatibilityVersion> mCompatVersions;
private final AlarmManager mAlarmManager;
private final PendingIntent mPendingIntent;
@@ -50,13 +52,13 @@
Context context,
DataStore dataStore,
CertificateTransparencyDownloader certificateTransparencyDownloader,
- CompatibilityVersion compatVersion,
- SignatureVerifier signatureVerifier) {
+ SignatureVerifier signatureVerifier,
+ Collection<CompatibilityVersion> compatVersions) {
mContext = context;
mDataStore = dataStore;
mCertificateTransparencyDownloader = certificateTransparencyDownloader;
- mCompatVersion = compatVersion;
mSignatureVerifier = signatureVerifier;
+ mCompatVersions = compatVersions;
mAlarmManager = context.getSystemService(AlarmManager.class);
mPendingIntent =
@@ -99,7 +101,9 @@
}
mDependenciesReady = false;
- mCompatVersion.delete();
+ for (CompatibilityVersion compatVersion : mCompatVersions) {
+ compatVersion.delete();
+ }
if (Config.DEBUG) {
Log.d(TAG, "CertificateTransparencyJob canceled.");
@@ -129,7 +133,6 @@
private void startDependencies() {
mDataStore.load();
- mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
mSignatureVerifier.loadAllowedKeys();
mContext.registerReceiver(
mCertificateTransparencyDownloader,
@@ -144,7 +147,6 @@
private void stopDependencies() {
mContext.unregisterReceiver(mCertificateTransparencyDownloader);
mSignatureVerifier.clearAllowedKeys();
- mCertificateTransparencyDownloader.clearCompatibilityVersions();
mDataStore.delete();
if (Config.DEBUG) {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index 2e910b2..5e530c7 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -30,6 +30,8 @@
import com.android.server.SystemService;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.concurrent.Executors;
/** Implementation of the Certificate Transparency service. */
@@ -51,8 +53,18 @@
/** Creates a new {@link CertificateTransparencyService} object. */
public CertificateTransparencyService(Context context) {
DataStore dataStore = new DataStore(Config.PREFERENCES_FILE);
-
SignatureVerifier signatureVerifier = new SignatureVerifier(context);
+ Collection<CompatibilityVersion> compatVersions =
+ Arrays.asList(
+ new CompatibilityVersion(
+ Config.COMPATIBILITY_VERSION_V1,
+ Config.URL_SIGNATURE_V1,
+ Config.URL_LOG_LIST_V1),
+ new CompatibilityVersion(
+ Config.COMPATIBILITY_VERSION_V2,
+ Config.URL_SIGNATURE_V2,
+ Config.URL_LOG_LIST_V2));
+
mCertificateTransparencyJob =
new CertificateTransparencyJob(
context,
@@ -62,13 +74,10 @@
dataStore,
new DownloadHelper(context),
signatureVerifier,
- new CertificateTransparencyLoggerImpl(dataStore)),
- new CompatibilityVersion(
- Config.COMPATIBILITY_VERSION,
- Config.URL_SIGNATURE,
- Config.URL_LOG_LIST,
- Config.CT_ROOT_DIRECTORY_PATH),
- signatureVerifier);
+ new CertificateTransparencyLoggerImpl(dataStore),
+ compatVersions),
+ signatureVerifier,
+ compatVersions);
}
/**
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
index e8a6e64..0a91963 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -23,6 +23,8 @@
import android.system.Os;
import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+
import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
import org.json.JSONException;
@@ -40,6 +42,8 @@
private static final String TAG = "CompatibilityVersion";
+ private static File sRootDirectory = new File(Config.CT_ROOT_DIRECTORY_PATH);
+
static final String LOGS_DIR_PREFIX = "logs-";
static final String LOGS_LIST_FILE_NAME = "log_list.json";
static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
@@ -48,23 +52,21 @@
private final String mMetadataUrl;
private final String mContentUrl;
- private final File mRootDirectory;
private final File mVersionDirectory;
private final File mCurrentLogsDirSymlink;
CompatibilityVersion(
- String compatVersion, String metadataUrl, String contentUrl, File rootDirectory) {
+ String compatVersion, String metadataUrl, String contentUrl) {
mCompatVersion = compatVersion;
mMetadataUrl = metadataUrl;
mContentUrl = contentUrl;
- mRootDirectory = rootDirectory;
- mVersionDirectory = new File(rootDirectory, compatVersion);
+ mVersionDirectory = new File(sRootDirectory, compatVersion);
mCurrentLogsDirSymlink = new File(mVersionDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
}
- CompatibilityVersion(
- String compatVersion, String metadataUrl, String contentUrl, String rootDirectoryPath) {
- this(compatVersion, metadataUrl, contentUrl, new File(rootDirectoryPath));
+ @VisibleForTesting
+ static void setRootDirectoryForTesting(File rootDirectory) {
+ sRootDirectory = rootDirectory;
}
/**
@@ -75,8 +77,8 @@
* @return true if the log list was installed successfully, false otherwise.
* @throws IOException if the list cannot be saved in the CT directory.
*/
- LogListUpdateStatus install(
- InputStream newContent, LogListUpdateStatus.Builder statusBuilder) throws IOException {
+ LogListUpdateStatus install(InputStream newContent, LogListUpdateStatus.Builder statusBuilder)
+ throws IOException {
String content = new String(newContent.readAllBytes(), UTF_8);
try {
JSONObject contentJson = new JSONObject(content);
@@ -98,7 +100,7 @@
// there's a bunch of steps. We create a new directory with the logs and then do
// an atomic update of the current symlink to point to the new directory.
// 1. Ensure the path to the root and version directories exist and are readable.
- DirectoryUtils.makeDir(mRootDirectory);
+ DirectoryUtils.makeDir(sRootDirectory);
DirectoryUtils.makeDir(mVersionDirectory);
File newLogsDir = new File(mVersionDirectory, LOGS_DIR_PREFIX + version);
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index 5fdba09..72b715a 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -33,18 +33,14 @@
private static final String PREFERENCES_FILE_NAME = "ct.preferences";
static final File PREFERENCES_FILE = new File(DEVICE_PROTECTED_DATA_DIR, PREFERENCES_FILE_NAME);
- // CT directory
+ // CT paths
static final String CT_ROOT_DIRECTORY_PATH = "/data/misc/keychain/ct/";
- static final String COMPATIBILITY_VERSION = "v1";
+ static final String URL_PREFIX = "https://www.gstatic.com/android/certificate_transparency/";
// Phenotype flags
static final String NAMESPACE_NETWORK_SECURITY = "network_security";
private static final String FLAGS_PREFIX = "CertificateTransparencyLogList__";
static final String FLAG_SERVICE_ENABLED = FLAGS_PREFIX + "service_enabled";
- static final String FLAG_CONTENT_URL = FLAGS_PREFIX + "content_url";
- static final String FLAG_METADATA_URL = FLAGS_PREFIX + "metadata_url";
- static final String FLAG_VERSION = FLAGS_PREFIX + "version";
- static final String FLAG_PUBLIC_KEY = FLAGS_PREFIX + "public_key";
// properties
static final String VERSION = "version";
@@ -53,9 +49,18 @@
static final String PUBLIC_KEY_DOWNLOAD_ID = "public_key_download_id";
static final String LOG_LIST_UPDATE_FAILURE_COUNT = "log_list_update_failure_count";
- // URLs
- static final String URL_PREFIX = "https://www.gstatic.com/android/certificate_transparency/";
- static final String URL_LOG_LIST = URL_PREFIX + "log_list.json";
- static final String URL_SIGNATURE = URL_PREFIX + "log_list.sig";
+ // Public Key URLs
static final String URL_PUBLIC_KEY = URL_PREFIX + "log_list.pub";
+
+ // Compatibility Version v1
+ static final String COMPATIBILITY_VERSION_V1 = "v1";
+ static final String URL_PREFIX_V1 = URL_PREFIX;
+ static final String URL_LOG_LIST_V1 = URL_PREFIX_V1 + "log_list.json";
+ static final String URL_SIGNATURE_V1 = URL_PREFIX_V1 + "log_list.sig";
+
+ // Compatibility Version v2
+ static final String COMPATIBILITY_VERSION_V2 = "v2";
+ static final String URL_PREFIX_V2 = URL_PREFIX + COMPATIBILITY_VERSION_V2 + "/";
+ static final String URL_LOG_LIST_V2 = URL_PREFIX_V2 + "log_list.json";
+ static final String URL_SIGNATURE_V2 = URL_PREFIX_V2 + "log_list.sig";
}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 22dc6ab..956bad5 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -60,6 +60,7 @@
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
+import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;
@@ -94,24 +95,25 @@
mContext = InstrumentationRegistry.getInstrumentation().getContext();
mDataStore = new DataStore(File.createTempFile("datastore-test", ".properties"));
mSignatureVerifier = new SignatureVerifier(mContext);
+
+ CompatibilityVersion.setRootDirectoryForTesting(mContext.getFilesDir());
+ mCompatVersion =
+ new CompatibilityVersion(
+ /* compatVersion= */ "v666",
+ Config.URL_SIGNATURE_V1,
+ Config.URL_LOG_LIST_V1);
mCertificateTransparencyDownloader =
new CertificateTransparencyDownloader(
mContext,
mDataStore,
new DownloadHelper(mDownloadManager),
mSignatureVerifier,
- mLogger);
- mCompatVersion =
- new CompatibilityVersion(
- /* compatVersion= */ "v666",
- Config.URL_SIGNATURE,
- Config.URL_LOG_LIST,
- mContext.getFilesDir());
+ mLogger,
+ Arrays.asList(mCompatVersion));
prepareDownloadManager();
mSignatureVerifier.addAllowedKey(mPublicKey);
mDataStore.load();
- mCertificateTransparencyDownloader.addCompatibilityVersion(mCompatVersion);
}
@After
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
index 2b8b3cd..0d15183 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
@@ -27,6 +27,7 @@
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.After;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -47,9 +48,16 @@
private final File mTestDir =
InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
- private final CompatibilityVersion mCompatVersion =
- new CompatibilityVersion(
- TEST_VERSION, Config.URL_SIGNATURE, Config.URL_LOG_LIST, mTestDir);
+
+ private CompatibilityVersion mCompatVersion;
+
+ @Before
+ public void setUp() {
+ CompatibilityVersion.setRootDirectoryForTesting(mTestDir);
+ mCompatVersion =
+ new CompatibilityVersion(
+ TEST_VERSION, Config.URL_SIGNATURE_V1, Config.URL_LOG_LIST_V1);
+ }
@After
public void tearDown() {
@@ -111,9 +119,7 @@
JSONObject logList = makeLogList(version, "i_am_content");
try (InputStream inputStream = asStream(logList)) {
- assertThat(
- mCompatVersion.install(
- inputStream, LogListUpdateStatus.builder()))
+ assertThat(mCompatVersion.install(inputStream, LogListUpdateStatus.builder()))
.isEqualTo(getSuccessfulUpdateStatus());
}
@@ -142,9 +148,7 @@
@Test
public void testCompatibilityVersion_deleteSuccessfully() throws Exception {
try (InputStream inputStream = asStream(makeLogList(/* version= */ "123"))) {
- assertThat(
- mCompatVersion.install(
- inputStream, LogListUpdateStatus.builder()))
+ assertThat(mCompatVersion.install(inputStream, LogListUpdateStatus.builder()))
.isEqualTo(getSuccessfulUpdateStatus());
}
@@ -156,9 +160,7 @@
@Test
public void testCompatibilityVersion_invalidLogList() throws Exception {
try (InputStream inputStream = new ByteArrayInputStream(("not_a_valid_list".getBytes()))) {
- assertThat(
- mCompatVersion.install(
- inputStream, LogListUpdateStatus.builder()))
+ assertThat(mCompatVersion.install(inputStream, LogListUpdateStatus.builder()))
.isEqualTo(LogListUpdateStatus.builder().setState(LOG_LIST_INVALID).build());
}
@@ -179,9 +181,7 @@
JSONObject newLogList = makeLogList(existingVersion, "i_am_the_real_content");
try (InputStream inputStream = asStream(newLogList)) {
- assertThat(
- mCompatVersion.install(
- inputStream, LogListUpdateStatus.builder()))
+ assertThat(mCompatVersion.install(inputStream, LogListUpdateStatus.builder()))
.isEqualTo(getSuccessfulUpdateStatus());
}
@@ -193,16 +193,12 @@
String existingVersion = "666";
JSONObject existingLogList = makeLogList(existingVersion, "i_was_installed_successfully");
try (InputStream inputStream = asStream(existingLogList)) {
- assertThat(
- mCompatVersion.install(
- inputStream, LogListUpdateStatus.builder()))
+ assertThat(mCompatVersion.install(inputStream, LogListUpdateStatus.builder()))
.isEqualTo(getSuccessfulUpdateStatus());
}
try (InputStream inputStream = asStream(makeLogList(existingVersion, "i_am_ignored"))) {
- assertThat(
- mCompatVersion.install(
- inputStream, LogListUpdateStatus.builder()))
+ assertThat(mCompatVersion.install(inputStream, LogListUpdateStatus.builder()))
.isEqualTo(
LogListUpdateStatus.builder()
.setState(VERSION_ALREADY_EXISTS)
diff --git a/service-t/Android.bp b/service-t/Android.bp
index ab38c7a..81378f5 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -109,13 +109,15 @@
":service-mdns-droidstubs",
],
exclude_srcs: [
+ "src/com/android/server/connectivity/mdns/internal/MdnsRealtimeScheduler.java",
"src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java",
- "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java",
"src/com/android/server/connectivity/mdns/MdnsAdvertiser.java",
"src/com/android/server/connectivity/mdns/MdnsAnnouncer.java",
"src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java",
"src/com/android/server/connectivity/mdns/MdnsProber.java",
"src/com/android/server/connectivity/mdns/MdnsRecordRepository.java",
+ "src/com/android/server/connectivity/mdns/SchedulerFactory.java",
+ "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java",
],
static_libs: [
"net-utils-device-common-mdns-standalone-build-test",
@@ -132,7 +134,10 @@
droidstubs {
name: "service-mdns-droidstubs",
- srcs: ["src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java"],
+ srcs: [
+ "src/com/android/server/connectivity/mdns/SchedulerFactory.java",
+ "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java",
+ ],
libs: [
"net-utils-device-common-mdns-standalone-build-test",
"service-connectivity-tiramisu-pre-jarjar",
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/DiscoveryExecutor.java b/service-t/src/com/android/server/connectivity/mdns/DiscoveryExecutor.java
index 21af1a1..99354f8 100644
--- a/service-t/src/com/android/server/connectivity/mdns/DiscoveryExecutor.java
+++ b/service-t/src/com/android/server/connectivity/mdns/DiscoveryExecutor.java
@@ -49,7 +49,13 @@
@NonNull
private final ArrayList<Pair<Runnable, Long>> mPendingTasks = new ArrayList<>();
- DiscoveryExecutor(@Nullable Looper defaultLooper) {
+ @GuardedBy("mPendingTasks")
+ @Nullable
+ Scheduler mScheduler;
+ @NonNull private final MdnsFeatureFlags mMdnsFeatureFlags;
+
+ DiscoveryExecutor(@Nullable Looper defaultLooper, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+ mMdnsFeatureFlags = mdnsFeatureFlags;
if (defaultLooper != null) {
this.mHandlerThread = null;
synchronized (mPendingTasks) {
@@ -62,7 +68,7 @@
synchronized (mPendingTasks) {
mHandler = new Handler(getLooper());
for (Pair<Runnable, Long> pendingTask : mPendingTasks) {
- mHandler.postDelayed(pendingTask.first, pendingTask.second);
+ executeDelayed(pendingTask.first, pendingTask.second);
}
mPendingTasks.clear();
}
@@ -95,15 +101,33 @@
/** Execute the given function after the specified amount of time elapses. */
public void executeDelayed(Runnable function, long delayMillis) {
final Handler handler;
+ final Scheduler scheduler;
synchronized (mPendingTasks) {
if (this.mHandler == null) {
mPendingTasks.add(Pair.create(function, delayMillis));
return;
} else {
handler = this.mHandler;
+ if (mMdnsFeatureFlags.mIsAccurateDelayCallbackEnabled
+ && this.mScheduler == null) {
+ this.mScheduler = SchedulerFactory.createScheduler(mHandler);
+ }
+ scheduler = this.mScheduler;
}
}
- handler.postDelayed(function, delayMillis);
+ if (scheduler != null) {
+ if (delayMillis == 0L) {
+ handler.post(function);
+ return;
+ }
+ if (HandlerUtils.isRunningOnHandlerThread(handler)) {
+ scheduler.postDelayed(function, delayMillis);
+ } else {
+ handler.post(() -> scheduler.postDelayed(function, delayMillis));
+ }
+ } else {
+ handler.postDelayed(function, delayMillis);
+ }
}
/** Shutdown the thread if necessary. */
@@ -111,6 +135,11 @@
if (this.mHandlerThread != null) {
this.mHandlerThread.quitSafely();
}
+ synchronized (mPendingTasks) {
+ if (mScheduler != null) {
+ mScheduler.close();
+ }
+ }
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 33bcb70..8cd3662 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -125,7 +125,7 @@
this.sharedLog = sharedLog;
this.perSocketServiceTypeClients = new PerSocketServiceTypeClients();
this.mdnsFeatureFlags = mdnsFeatureFlags;
- this.discoveryExecutor = new DiscoveryExecutor(socketClient.getLooper());
+ this.discoveryExecutor = new DiscoveryExecutor(socketClient.getLooper(), mdnsFeatureFlags);
}
/**
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/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 56d4b9a..95f4fff 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -37,7 +37,6 @@
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DnsUtils;
-import com.android.net.module.util.RealtimeScheduler;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -96,9 +95,9 @@
private final boolean removeServiceAfterTtlExpires =
MdnsConfigs.removeServiceAfterTtlExpires();
private final Clock clock;
- // Use RealtimeScheduler for query scheduling, which allows for more accurate sending of
+ // Use MdnsRealtimeScheduler for query scheduling, which allows for more accurate sending of
// queries.
- @Nullable private final RealtimeScheduler realtimeScheduler;
+ @Nullable private final Scheduler scheduler;
@Nullable private MdnsSearchOptions searchOptions;
@@ -193,7 +192,7 @@
sharedLog.log(String.format("Query sent with transactionId: %d. "
+ "Next run: sessionId: %d, in %d ms",
sentResult.transactionId, args.sessionId, timeToNextTaskMs));
- if (realtimeScheduler != null) {
+ if (scheduler != null) {
setDelayedTask(args, timeToNextTaskMs);
} else {
dependencies.sendMessageDelayed(
@@ -264,11 +263,11 @@
}
/**
- * @see RealtimeScheduler
+ * @see Scheduler
*/
@Nullable
- public RealtimeScheduler createRealtimeScheduler(@NonNull Handler handler) {
- return new RealtimeScheduler(handler);
+ public Scheduler createScheduler(@NonNull Handler handler) {
+ return SchedulerFactory.createScheduler(handler);
}
}
@@ -317,8 +316,8 @@
this.mdnsQueryScheduler = new MdnsQueryScheduler();
this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
this.featureFlags = featureFlags;
- this.realtimeScheduler = featureFlags.isAccurateDelayCallbackEnabled()
- ? dependencies.createRealtimeScheduler(handler) : null;
+ this.scheduler = featureFlags.isAccurateDelayCallbackEnabled()
+ ? dependencies.createScheduler(handler) : null;
}
/**
@@ -328,8 +327,8 @@
removeScheduledTask();
mdnsQueryScheduler.cancelScheduledRun();
serviceCache.unregisterServiceExpiredCallback(cacheKey);
- if (realtimeScheduler != null) {
- realtimeScheduler.close();
+ if (scheduler != null) {
+ scheduler.close();
}
}
@@ -339,8 +338,8 @@
}
private void setDelayedTask(ScheduledQueryTaskArgs args, long timeToNextTaskMs) {
- realtimeScheduler.removeDelayedMessage(EVENT_START_QUERYTASK);
- realtimeScheduler.sendDelayedMessage(
+ scheduler.removeDelayedMessage(EVENT_START_QUERYTASK);
+ scheduler.sendDelayedMessage(
handler.obtainMessage(EVENT_START_QUERYTASK, args), timeToNextTaskMs);
}
@@ -404,7 +403,7 @@
final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
sharedLog.log(String.format("Schedule a query. Next run: sessionId: %d, in %d ms",
args.sessionId, timeToNextTaskMs));
- if (realtimeScheduler != null) {
+ if (scheduler != null) {
setDelayedTask(args, timeToNextTaskMs);
} else {
dependencies.sendMessageDelayed(
@@ -451,8 +450,8 @@
}
private void removeScheduledTask() {
- if (realtimeScheduler != null) {
- realtimeScheduler.removeDelayedMessage(EVENT_START_QUERYTASK);
+ if (scheduler != null) {
+ scheduler.removeDelayedMessage(EVENT_START_QUERYTASK);
} else {
dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
}
@@ -541,8 +540,8 @@
}
}
}
- final boolean hasScheduledTask = realtimeScheduler != null
- ? realtimeScheduler.hasDelayedMessage(EVENT_START_QUERYTASK)
+ final boolean hasScheduledTask = scheduler != null
+ ? scheduler.hasDelayedMessage(EVENT_START_QUERYTASK)
: dependencies.hasMessages(handler, EVENT_START_QUERYTASK);
if (hasScheduledTask) {
final long now = clock.elapsedRealtime();
@@ -556,7 +555,7 @@
final long timeToNextTaskMs = calculateTimeToNextTask(args, now);
sharedLog.log(String.format("Reschedule a query. Next run: sessionId: %d, in %d ms",
args.sessionId, timeToNextTaskMs));
- if (realtimeScheduler != null) {
+ if (scheduler != null) {
setDelayedTask(args, timeToNextTaskMs);
} else {
dependencies.sendMessageDelayed(
diff --git a/service-t/src/com/android/server/connectivity/mdns/Scheduler.java b/service-t/src/com/android/server/connectivity/mdns/Scheduler.java
new file mode 100644
index 0000000..85a8e76
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/Scheduler.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.os.Message;
+
+import androidx.annotation.NonNull;
+
+/**
+ * The interface for scheduler.
+ */
+public interface Scheduler {
+ /**
+ * Set a message to be sent after a specified delay.
+ */
+ boolean sendDelayedMessage(@NonNull Message message, long delayMs);
+
+ /**
+ * Remove a scheduled message.
+ */
+ void removeDelayedMessage(int what);
+
+ /**
+ * Check if there is a scheduled message.
+ */
+ boolean hasDelayedMessage(int what);
+
+ /**
+ * Set a runnable to be executed after a specified delay.
+ */
+ boolean postDelayed(@NonNull Runnable runnable, long delayMs);
+
+ /**
+ * Close this object.
+ */
+ void close();
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/SchedulerFactory.java b/service-t/src/com/android/server/connectivity/mdns/SchedulerFactory.java
new file mode 100644
index 0000000..1cc9a6b
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/SchedulerFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+
+import com.android.server.connectivity.mdns.internal.MdnsRealtimeScheduler;
+
+/**
+ * The factory class for creating a scheduler.
+ */
+public class SchedulerFactory {
+
+ /**
+ * Creates an realtime delay callback.
+ */
+ public static Scheduler createScheduler(@NonNull Handler handler) {
+ return new MdnsRealtimeScheduler(handler);
+ }
+
+ private SchedulerFactory() {
+ }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/internal/MdnsRealtimeScheduler.java b/service-t/src/com/android/server/connectivity/mdns/internal/MdnsRealtimeScheduler.java
new file mode 100644
index 0000000..eff7085
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/internal/MdnsRealtimeScheduler.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns.internal;
+
+import android.os.Handler;
+import android.os.Message;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.RealtimeScheduler;
+import com.android.server.connectivity.mdns.Scheduler;
+
+/**
+ * The delay callback for delivering scheduled tasks accurately.
+ */
+public class MdnsRealtimeScheduler extends RealtimeScheduler implements
+ Scheduler {
+ private static final String TAG = MdnsRealtimeScheduler.class.getSimpleName();
+
+ public MdnsRealtimeScheduler(@NonNull Handler handler) {
+ super(handler);
+ }
+
+ public boolean sendDelayedMessage(@NonNull Message message, long delayMs) {
+ return super.sendDelayedMessage(message, delayMs);
+ }
+
+ public void removeDelayedMessage(int what) {
+ super.removeDelayedMessage(what);
+ }
+
+ public boolean hasDelayedMessage(int what) {
+ return super.hasDelayedMessage(what);
+ }
+
+ public boolean postDelayed(@NonNull Runnable runnable, long delayMs) {
+ return super.postDelayed(runnable, delayMs);
+ }
+
+ public void close() {
+ super.close();
+ }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java
index 5f66f47..5ff708d 100644
--- a/service-t/src/com/android/server/net/NetworkStatsFactory.java
+++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java
@@ -21,7 +21,6 @@
import static android.net.NetworkStats.UID_ALL;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.Context;
import android.net.NetworkStats;
import android.net.UnderlyingNetworkInfo;
@@ -31,6 +30,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.BpfNetMaps;
+import com.android.server.connectivity.InterfaceTracker;
import java.io.IOException;
import java.net.ProtocolException;
@@ -108,7 +108,7 @@
/** Create a new {@link BpfNetMaps}. */
public BpfNetMaps createBpfNetMaps(@NonNull Context ctx) {
- return new BpfNetMaps(ctx);
+ return new BpfNetMaps(ctx, new InterfaceTracker(ctx));
}
}
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
index d1d9e52..128a98f 100644
--- a/service/ServiceConnectivityResources/res/values/config_thread.xml
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -26,6 +26,10 @@
-->
<bool name="config_thread_default_enabled">true</bool>
+ <!-- Sets to {@code true} to enable Thread Border Router on the device by default.
+ -->
+ <bool name="config_thread_border_router_default_enabled">false</bool>
+
<!-- Whether to use location APIs in the algorithm to determine country code or not.
If disabled, will use other sources (telephony, wifi, etc) to determine device location for
Thread Network regulatory purposes.
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 36c0cf9..25c0617 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -49,6 +49,8 @@
import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UNINSTALLED;
import static com.android.server.connectivity.NetworkPermissions.TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.StatsManager;
import android.content.Context;
import android.net.BpfNetMapsUtils;
@@ -85,12 +87,14 @@
import com.android.net.module.util.bpf.IngressDiscardKey;
import com.android.net.module.util.bpf.IngressDiscardValue;
import com.android.net.module.util.bpf.LocalNetAccessKey;
+import com.android.server.connectivity.InterfaceTracker;
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
@@ -142,6 +146,7 @@
Pair.create(TRAFFIC_PERMISSION_INTERNET, "PERMISSION_INTERNET"),
Pair.create(TRAFFIC_PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
);
+ private final InterfaceTracker mInterfaceTracker;
/**
* Set configurationMap for test.
@@ -423,23 +428,27 @@
/** Constructor used after T that doesn't need to use netd anymore. */
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
- public BpfNetMaps(final Context context) {
- this(context, null);
+ public BpfNetMaps(final Context context, @NonNull final InterfaceTracker interfaceTracker) {
+ this(context, null, interfaceTracker);
if (!SdkLevel.isAtLeastT()) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
}
- public BpfNetMaps(final Context context, final INetd netd) {
- this(context, netd, new Dependencies());
+ public BpfNetMaps(final Context context, final INetd netd, @NonNull final InterfaceTracker
+ interfaceTracker) {
+ this(context, netd, new Dependencies(), interfaceTracker);
}
@VisibleForTesting
- public BpfNetMaps(final Context context, final INetd netd, final Dependencies deps) {
+ public BpfNetMaps(final Context context, final INetd netd, final Dependencies deps,
+ @NonNull final InterfaceTracker interfaceTracker) {
+ Objects.requireNonNull(interfaceTracker);
if (SdkLevel.isAtLeastT()) {
ensureInitialized(context);
}
mNetd = netd;
mDeps = deps;
+ mInterfaceTracker = interfaceTracker;
}
private void maybeThrow(final int err, final String msg) {
@@ -902,7 +911,7 @@
* @param isAllowed is the local network call allowed or blocked.
*/
@RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
- public void addLocalNetAccess(final int lpmBitlen, final String iface,
+ public void addLocalNetAccess(final int lpmBitlen, @Nullable final String iface,
final InetAddress address, final int protocol, final int remotePort,
final boolean isAllowed) {
throwIfPre25Q2("addLocalNetAccess is not available on pre-B devices");
@@ -910,7 +919,7 @@
if (iface == null) {
ifIndex = 0;
} else {
- ifIndex = mDeps.getIfIndex(iface);
+ ifIndex = mInterfaceTracker.getInterfaceIndex(iface);
}
if (ifIndex == 0) {
Log.e(TAG, "Failed to get if index, skip addLocalNetAccess for " + address
@@ -937,14 +946,14 @@
* @param remotePort src/dst port for ingress/egress
*/
@RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
- public void removeLocalNetAccess(final int lpmBitlen, final String iface,
+ public void removeLocalNetAccess(final int lpmBitlen, @Nullable final String iface,
final InetAddress address, final int protocol, final int remotePort) {
throwIfPre25Q2("removeLocalNetAccess is not available on pre-B devices");
final int ifIndex;
if (iface == null) {
ifIndex = 0;
} else {
- ifIndex = mDeps.getIfIndex(iface);
+ ifIndex = mInterfaceTracker.getInterfaceIndex(iface);
}
if (ifIndex == 0) {
Log.e(TAG, "Failed to get if index, skip removeLocalNetAccess for " + address
@@ -973,14 +982,14 @@
* is not local network or if configuration is allowed like local dns servers.
*/
@RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
- public boolean getLocalNetAccess(final int lpmBitlen, final String iface,
+ public boolean getLocalNetAccess(final int lpmBitlen, @Nullable final String iface,
final InetAddress address, final int protocol, final int remotePort) {
throwIfPre25Q2("getLocalNetAccess is not available on pre-B devices");
final int ifIndex;
if (iface == null) {
ifIndex = 0;
} else {
- ifIndex = mDeps.getIfIndex(iface);
+ ifIndex = mInterfaceTracker.getInterfaceIndex(iface);
}
if (ifIndex == 0) {
Log.e(TAG, "Failed to get if index, returning default from getLocalNetAccess for "
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index b2e49e7..3ce3f02 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -365,6 +365,7 @@
import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
import com.android.server.connectivity.DscpPolicyTracker;
import com.android.server.connectivity.FullScore;
+import com.android.server.connectivity.InterfaceTracker;
import com.android.server.connectivity.InvalidTagException;
import com.android.server.connectivity.KeepaliveResourceUtil;
import com.android.server.connectivity.KeepaliveTracker;
@@ -577,6 +578,7 @@
private final NetworkStatsManager mStatsManager;
private final NetworkPolicyManager mPolicyManager;
private final BpfNetMaps mBpfNetMaps;
+ private final InterfaceTracker mInterfaceTracker;
/**
* TestNetworkService (lazily) created upon first usage. Locked to prevent creation of multiple
@@ -1662,8 +1664,17 @@
* @param netd a netd binder
* @return BpfNetMaps implementation.
*/
- public BpfNetMaps getBpfNetMaps(Context context, INetd netd) {
- return new BpfNetMaps(context, netd);
+ public BpfNetMaps getBpfNetMaps(Context context, INetd netd,
+ InterfaceTracker interfaceTracker) {
+ return new BpfNetMaps(context, netd, interfaceTracker);
+ }
+
+ /**
+ * Get the InterfaceTracker implementation to use in ConnectivityService.
+ * @return InterfaceTracker implementation.
+ */
+ public InterfaceTracker getInterfaceTracker(Context context) {
+ return new InterfaceTracker(context);
}
/**
@@ -1886,7 +1897,8 @@
mWakeUpMask = mask;
mNetd = netd;
- mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd);
+ mInterfaceTracker = mDeps.getInterfaceTracker(mContext);
+ mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd, mInterfaceTracker);
mHandlerThread = mDeps.makeHandlerThread("ConnectivityServiceThread");
mPermissionMonitorDeps = mPermDeps;
mPermissionMonitor =
@@ -9605,8 +9617,6 @@
updateIngressToVpnAddressFiltering(newLp, oldLp, networkAgent);
- updateLocalNetworkAddresses(newLp, oldLp);
-
updateMtu(newLp, oldLp);
// TODO - figure out what to do for clat
// for (LinkProperties lp : newLp.getStackedLinks()) {
@@ -9769,16 +9779,23 @@
wakeupModifyInterface(iface, nai, true);
mDeps.reportNetworkInterfaceForTransports(mContext, iface,
nai.networkCapabilities.getTransportTypes());
+ mInterfaceTracker.addInterface(iface);
} catch (Exception e) {
logw("Exception adding interface: " + e);
}
}
}
+
+ // The local network addresses needs to be updated before interfaces are removed because
+ // modifying bpf map local_net_access requires mapping interface name to index.
+ updateLocalNetworkAddresses(newLp, oldLp);
+
for (final String iface : interfaceDiff.removed) {
try {
if (DBG) log("Removing iface " + iface + " from network " + netId);
wakeupModifyInterface(iface, nai, false);
mRoutingCoordinatorService.removeInterfaceFromNetwork(netId, iface);
+ mInterfaceTracker.removeInterface(iface);
} catch (Exception e) {
loge("Exception removing interface: " + e);
}
diff --git a/service/src/com/android/server/connectivity/InterfaceTracker.java b/service/src/com/android/server/connectivity/InterfaceTracker.java
new file mode 100644
index 0000000..0b4abeb
--- /dev/null
+++ b/service/src/com/android/server/connectivity/InterfaceTracker.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.system.Os;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.BpfNetMaps;
+
+import java.util.Map;
+
+/**
+ * InterfaceTracker is responsible for providing interface mapping and tracking.
+ * @hide
+ */
+public class InterfaceTracker {
+ static {
+ if (BpfNetMaps.isAtLeast25Q2()) {
+ System.loadLibrary("service-connectivity");
+ }
+ }
+ private static final String TAG = "InterfaceTracker";
+ private final Dependencies mDeps;
+ private final Map<String, Integer> mInterfaceMap;
+
+ public InterfaceTracker(final Context context) {
+ this(context, new Dependencies());
+ }
+
+ @VisibleForTesting
+ public InterfaceTracker(final Context context, final Dependencies deps) {
+ this.mInterfaceMap = new ArrayMap<>();
+ this.mDeps = deps;
+ }
+
+ /**
+ * To add interface to tracking
+ * @param interfaceName name of interface added.
+ */
+ public void addInterface(@Nullable final String interfaceName) {
+ final int interfaceIndex;
+ if (interfaceName == null) {
+ interfaceIndex = 0;
+ } else {
+ interfaceIndex = mDeps.getIfIndex(interfaceName);
+ }
+ if (interfaceIndex == 0) {
+ Log.e(TAG, "Failed to get interface index for " + interfaceName);
+ return;
+ }
+ synchronized (mInterfaceMap) {
+ mInterfaceMap.put(interfaceName, interfaceIndex);
+ }
+ }
+
+ /**
+ * To remove interface from tracking
+ * @param interfaceName name of interface removed.
+ * @return true if the value was present and now removed.
+ */
+ public boolean removeInterface(@Nullable final String interfaceName) {
+ if (interfaceName == null) return false;
+ synchronized (mInterfaceMap) {
+ return mInterfaceMap.remove(interfaceName) != null;
+ }
+ }
+
+ /**
+ * Get interface index from interface name.
+ * @param interfaceName name of interface
+ * @return interface index for given interface name or 0 if interface is not found.
+ */
+ public int getInterfaceIndex(@Nullable final String interfaceName) {
+ final int interfaceIndex;
+ if (interfaceName != null) {
+ synchronized (mInterfaceMap) {
+ interfaceIndex = mInterfaceMap.getOrDefault(interfaceName, 0);
+ }
+ } else {
+ interfaceIndex = 0;
+ }
+ return interfaceIndex;
+ }
+
+ /**
+ * Dependencies of InterfaceTracker, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * Get interface index.
+ */
+ public int getIfIndex(final String ifName) {
+ return Os.if_nametoindex(ifName);
+ }
+
+ /**
+ * Get interface name
+ */
+ public String getIfName(final int ifIndex) {
+ return Os.if_indextoname(ifIndex);
+ }
+
+ }
+}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 8034e57..abfc447 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -81,17 +81,6 @@
},
}
-java_defaults {
- name: "lib_mockito_extended",
- static_libs: [
- "mockito-target-extended-minus-junit4",
- ],
- jni_libs: [
- "libdexmakerjvmtiagent",
- "libstaticjvmtiagent",
- ],
-}
-
java_library {
name: "net-utils-dnspacket-common",
srcs: [
@@ -438,11 +427,7 @@
srcs: [
"device/com/android/net/module/util/FdEventsReader.java",
"device/com/android/net/module/util/HandlerUtils.java",
- "device/com/android/net/module/util/JniUtil.java",
- "device/com/android/net/module/util/RealtimeScheduler.java",
"device/com/android/net/module/util/SharedLog.java",
- "device/com/android/net/module/util/ServiceConnectivityJni.java",
- "device/com/android/net/module/util/TimerFdUtils.java",
"framework/com/android/net/module/util/ByteUtils.java",
"framework/com/android/net/module/util/CollectionUtils.java",
"framework/com/android/net/module/util/DnsUtils.java",
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
index c19a124..5d49fa3 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -22,7 +22,6 @@
import static com.android.net.module.util.netlink.NetlinkConstants.IFF_UP;
import static com.android.net.module.util.netlink.NetlinkConstants.RTM_GETLINK;
import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWLINK;
-import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST_ACK;
import android.net.MacAddress;
@@ -307,7 +306,7 @@
}
return RtNetlinkLinkMessage.build(
- new StructNlMsgHdr(0, RTM_GETLINK, NLM_F_REQUEST, sequenceNumber),
+ new StructNlMsgHdr(0, RTM_GETLINK, NLM_F_REQUEST_ACK, sequenceNumber),
new StructIfinfoMsg((short) AF_UNSPEC, (short) 0, interfaceIndex, 0, 0),
DEFAULT_MTU, null, null);
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
index 13710b1..08cab03 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -290,7 +290,7 @@
@Test
public void testCreateGetLinkMessage() {
final String expectedHexBytes =
- "20000000120001006824000000000000" // struct nlmsghdr
+ "20000000120005006824000000000000" // struct nlmsghdr
+ "00000000080000000000000000000000"; // struct ifinfomsg
final String interfaceName = "wlan0";
final int interfaceIndex = 8;
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index ec486fb..b59ccc6 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -25,7 +25,6 @@
],
defaults: [
"framework-connectivity-test-defaults",
- "lib_mockito_extended",
],
libs: [
"androidx.annotation_annotation",
@@ -36,6 +35,7 @@
"collector-device-lib",
"kotlin-reflect",
"libnanohttpd",
+ "mockito-target-minus-junit4",
"net-tests-utils-host-device-common",
"net-utils-device-common",
"net-utils-device-common-async",
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
index 4b9429b..8a255c6 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -224,7 +224,7 @@
// Echo the current pid, and replace it (with exec) with the tcpdump process, so the
// tcpdump pid is known.
writer.write(
- "echo $$; exec su 0 tcpdump -n -i any -U -xx".encodeToByteArray()
+ "echo $$; exec su 0 tcpdump -n -i any -l -xx".encodeToByteArray()
)
}
val reader = FileReader(stdout.fileDescriptor).buffered()
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index 49ffad6..c2ad18e 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -18,6 +18,7 @@
from mobly.controllers import android_device
from mobly.controllers.android_device_lib.adb import AdbError
from net_tests_utils.host.python import adb_utils, assert_utils
+import functools
class PatternNotFoundException(Exception):
@@ -400,6 +401,20 @@
f" {expected_version}",
)
+def at_least_B():
+ def decorator(test_function):
+ @functools.wraps(test_function)
+ def wrapper(self, *args, **kwargs):
+ asserts.abort_class_if(
+ (not hasattr(self, 'client')) or (not hasattr(self.client, 'isAtLeastB')),
+ "client device is not B+"
+ )
+
+ asserts.abort_class_if(not self.client.isAtLeastB(), "not B+")
+ return test_function(self, *args, **kwargs)
+ return wrapper
+ return decorator
+
class AdbOutputHandler:
def __init__(self, ad, cmd):
self._ad = ad
diff --git a/staticlibs/testutils/host/python/multi_devices_test_base.py b/staticlibs/testutils/host/python/multi_devices_test_base.py
index 677329a..72bac0c 100644
--- a/staticlibs/testutils/host/python/multi_devices_test_base.py
+++ b/staticlibs/testutils/host/python/multi_devices_test_base.py
@@ -53,3 +53,4 @@
raise_on_exception=True,
)
self.client = self.clientDevice.connectivity_multi_devices_snippet
+ self.server = self.serverDevice.connectivity_multi_devices_snippet
diff --git a/tests/cts/multidevices/Android.bp b/tests/cts/multidevices/Android.bp
index c730b86..00fb934 100644
--- a/tests/cts/multidevices/Android.bp
+++ b/tests/cts/multidevices/Android.bp
@@ -29,6 +29,7 @@
libs: [
"absl-py",
"mobly",
+ "scapy",
"net-tests-utils-host-python-common",
],
test_suites: [
diff --git a/tests/cts/multidevices/apfv6_test.py b/tests/cts/multidevices/apfv6_test.py
index fc732d2..61f1bfc 100644
--- a/tests/cts/multidevices/apfv6_test.py
+++ b/tests/cts/multidevices/apfv6_test.py
@@ -13,6 +13,8 @@
# limitations under the License.
from mobly import asserts
+from scapy.layers.inet import IP, ICMP
+from scapy.layers.l2 import Ether
from net_tests_utils.host.python import apf_test_base, apf_utils, adb_utils, assert_utils, packet_utils
APFV6_VERSION = 6000
@@ -82,3 +84,18 @@
self.send_packet_and_expect_reply_received(
arp_request, "DROPPED_ARP_REQUEST_REPLIED", arp_reply
)
+
+ @apf_utils.at_least_B()
+ def test_ipv4_icmp_echo_request_offload(self):
+ eth = Ether(src=self.server_mac_address, dst=self.client_mac_address)
+ ip = IP(src=self.server_ipv4_addresses[0], dst=self.client_ipv4_addresses[0])
+ icmp = ICMP(id=1, seq=123)
+ echo_request = bytes(eth/ip/icmp/b"hello").hex()
+
+ eth = Ether(src=self.client_mac_address, dst=self.server_mac_address)
+ ip = IP(src=self.client_ipv4_addresses[0], dst=self.server_ipv4_addresses[0])
+ icmp = ICMP(type=0, id=1, seq=123)
+ expected_echo_reply = bytes(eth/ip/icmp/b"hello").hex()
+ self.send_packet_and_expect_reply_received(
+ echo_request, "DROPPED_IPV4_PING_REQUEST_REPLIED", expected_echo_reply
+ )
\ No newline at end of file
diff --git a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
index 252052e..e1c6bf1 100644
--- a/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
+++ b/tests/cts/multidevices/snippet/ConnectivityMultiDevicesSnippet.kt
@@ -33,6 +33,8 @@
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.net.wifi.WifiSsid
+import android.os.Build.VERSION.CODENAME
+import android.os.Build.VERSION.SDK_INT
import androidx.test.platform.app.InstrumentationRegistry
import com.android.compatibility.common.util.PropertyUtil
import com.android.modules.utils.build.SdkLevel
@@ -62,6 +64,17 @@
cbHelper.unregisterAll()
}
+ private fun isAtLeastPreReleaseCodename(codeName: String): Boolean {
+ // Special case "REL", which means the build is not a pre-release build.
+ if ("REL".equals(CODENAME)) {
+ return false
+ }
+
+ // Otherwise lexically compare them. Return true if the build codename is equal to or
+ // greater than the requested codename.
+ return CODENAME.compareTo(codeName) >= 0
+ }
+
@Rpc(description = "Check whether the device has wifi feature.")
fun hasWifiFeature() = pm.hasSystemFeature(FEATURE_WIFI)
@@ -77,6 +90,11 @@
@Rpc(description = "Return whether the Sdk level is at least V.")
fun isAtLeastV() = SdkLevel.isAtLeastV()
+ @Rpc(description = "Check whether the device is at least B.")
+ fun isAtLeastB(): Boolean {
+ return SDK_INT >= 36 || (SDK_INT == 35 && isAtLeastPreReleaseCodename("Baklava"))
+ }
+
@Rpc(description = "Return the API level that the VSR requirement must be fulfilled.")
fun getVsrApiLevel() = PropertyUtil.getVsrApiLevel()
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 8fcc703..5e035a2 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -172,6 +172,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.argThat
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.doReturn
@@ -1066,7 +1067,20 @@
fun testAgentStartsInConnecting() {
val mockContext = mock(Context::class.java)
val mockCm = mock(ConnectivityManager::class.java)
+ val mockedResult = ConnectivityManager.MockHelpers.registerNetworkAgentResult(
+ mock(Network::class.java),
+ mock(INetworkAgentRegistry::class.java)
+ )
doReturn(mockCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE)
+ doReturn(mockedResult).`when`(mockCm).registerNetworkAgent(
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ anyInt()
+ )
val agent = createNetworkAgent(mockContext)
agent.register()
verify(mockCm).registerNetworkAgent(
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 9e49926..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;
@@ -73,7 +74,6 @@
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiSsid;
-import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.ResultReceiver;
@@ -86,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;
@@ -235,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()) {
@@ -381,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 (Build.VERSION.SDK_INT <= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ if (!SdkLevel.isAtLeastB()) {
+ final String wifiTetheringIface = tetheredIface.getInterface();
try {
final int ret = runAsShell(TETHER_PRIVILEGED,
() -> mTM.tether(wifiTetheringIface));
@@ -456,32 +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 Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM
- && 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
@@ -494,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);
@@ -636,7 +705,7 @@
@Test
public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
- assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ assumeTrue(SdkLevel.isAtLeastB());
assertThrows(UnsupportedOperationException.class, () -> mTM.tether("iface"));
assertThrows(UnsupportedOperationException.class, () -> mTM.untether("iface"));
}
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 437eb81..de39215 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -55,11 +55,11 @@
import com.android.networkstack.apishim.TelephonyManagerShimImpl
import com.android.server.BpfNetMaps
import com.android.server.ConnectivityService
-import com.android.server.L2capNetworkProvider
import com.android.server.NetworkAgentWrapper
import com.android.server.TestNetIdManager
import com.android.server.connectivity.CarrierPrivilegeAuthenticator
import com.android.server.connectivity.ConnectivityResources
+import com.android.server.connectivity.InterfaceTracker
import com.android.server.connectivity.MockableSystemProperties
import com.android.server.connectivity.MultinetworkPolicyTracker
import com.android.server.connectivity.PermissionMonitor
@@ -114,6 +114,8 @@
@Mock
private lateinit var netd: INetd
@Mock
+ private lateinit var interfaceTracker: InterfaceTracker
+ @Mock
private lateinit var dnsResolver: IDnsResolver
@Mock
private lateinit var systemConfigManager: SystemConfigManager
@@ -140,11 +142,15 @@
private val realContext get() = InstrumentationRegistry.getInstrumentation().context
private val httpProbeUrl get() =
- realContext.getResources().getString(com.android.server.net.integrationtests.R.string
- .config_captive_portal_http_url)
+ realContext.getResources().getString(
+ com.android.server.net.integrationtests.R.string
+ .config_captive_portal_http_url
+ )
private val httpsProbeUrl get() =
- realContext.getResources().getString(com.android.server.net.integrationtests.R.string
- .config_captive_portal_https_url)
+ realContext.getResources().getString(
+ com.android.server.net.integrationtests.R.string
+ .config_captive_portal_https_url
+ )
private class InstrumentationServiceConnection : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
@@ -166,12 +172,19 @@
fun setUpClass() {
val intent = Intent(realContext, NetworkStackInstrumentationService::class.java)
intent.action = INetworkStackInstrumentation::class.qualifiedName
- assertTrue(realContext.bindService(intent, InstrumentationServiceConnection(),
- BIND_AUTO_CREATE or BIND_IMPORTANT),
- "Error binding to instrumentation service")
- assertTrue(bindingCondition.block(SERVICE_BIND_TIMEOUT_MS),
+ assertTrue(
+ realContext.bindService(
+ intent,
+ InstrumentationServiceConnection(),
+ BIND_AUTO_CREATE or BIND_IMPORTANT
+ ),
+ "Error binding to instrumentation service"
+ )
+ assertTrue(
+ bindingCondition.block(SERVICE_BIND_TIMEOUT_MS),
"Timed out binding to instrumentation service " +
- "after $SERVICE_BIND_TIMEOUT_MS ms")
+ "after $SERVICE_BIND_TIMEOUT_MS ms"
+ )
}
}
@@ -201,7 +214,8 @@
// We don't test the actual notification value strings, so just return an empty array.
// It doesn't matter what the values are as long as it's not null.
doReturn(emptyArray<String>()).`when`(resources).getStringArray(
- R.array.network_switch_type_name)
+ R.array.network_switch_type_name
+ )
doReturn(1).`when`(resources).getInteger(R.integer.config_networkAvoidBadWifi)
doReturn(R.array.config_networkSupportedKeepaliveCount).`when`(resources)
.getIdentifier(eq("config_networkSupportedKeepaliveCount"), eq("array"), any())
@@ -223,7 +237,13 @@
}
private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService(
- context, dnsResolver, log, netd, deps, PermissionMonitorDependencies())
+ context,
+ dnsResolver,
+ log,
+ netd,
+ deps,
+ PermissionMonitorDependencies()
+ )
private inner class TestDependencies : ConnectivityService.Dependencies() {
override fun getNetworkStack() = networkStackClient
@@ -231,7 +251,11 @@
mock(ProxyTracker::class.java)
override fun getSystemProperties() = mock(MockableSystemProperties::class.java)
override fun makeNetIdManager() = TestNetIdManager()
- override fun getBpfNetMaps(context: Context?, netd: INetd?) = mock(BpfNetMaps::class.java)
+ override fun getBpfNetMaps(
+ context: Context?,
+ netd: INetd?,
+ interfaceTracker: InterfaceTracker?
+ ) = mock(BpfNetMaps::class.java)
.also {
doReturn(PERMISSION_INTERNET).`when`(it).getNetPermForUid(anyInt())
}
@@ -241,13 +265,17 @@
c: Context,
h: Handler,
r: Runnable
- ) = MultinetworkPolicyTracker(c, h, r,
+ ) = MultinetworkPolicyTracker(
+ c,
+ h,
+ r,
object : MultinetworkPolicyTracker.Dependencies() {
override fun getResourcesForActiveSubId(
connResources: ConnectivityResources,
activeSubId: Int
) = resources
- })
+ }
+ )
override fun makeHandlerThread(tag: String): HandlerThread =
super.makeHandlerThread(tag).also { handlerThreads.add(it) }
@@ -259,13 +287,18 @@
listener: BiConsumer<Int, Int>,
handler: Handler
): CarrierPrivilegeAuthenticator {
- return CarrierPrivilegeAuthenticator(context,
+ return CarrierPrivilegeAuthenticator(
+ context,
object : CarrierPrivilegeAuthenticator.Dependencies() {
override fun makeHandlerThread(): HandlerThread =
super.makeHandlerThread().also { handlerThreads.add(it) }
},
- tm, TelephonyManagerShimImpl.newInstance(tm),
- requestRestrictedWifiEnabled, listener, handler)
+ tm,
+ TelephonyManagerShimImpl.newInstance(tm),
+ requestRestrictedWifiEnabled,
+ listener,
+ handler
+ )
}
override fun makeSatelliteAccessController(
@@ -273,7 +306,8 @@
updateSatellitePreferredUid: Consumer<MutableSet<Int>>?,
connectivityServiceInternalHandler: Handler
): SatelliteAccessController? = mock(
- SatelliteAccessController::class.java)
+ SatelliteAccessController::class.java
+ )
override fun makeL2capNetworkProvider(context: Context) = null
}
@@ -305,8 +339,12 @@
nsInstrumentation.addHttpResponse(HttpResponse(httpProbeUrl, responseCode = 204))
nsInstrumentation.addHttpResponse(HttpResponse(httpsProbeUrl, responseCode = 204))
- val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, LinkProperties(), null /* ncTemplate */,
- context)
+ val na = NetworkAgentWrapper(
+ TRANSPORT_CELLULAR,
+ LinkProperties(),
+ null /* ncTemplate */,
+ context
+ )
networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
na.addCapability(NET_CAPABILITY_INTERNET)
@@ -344,7 +382,8 @@
| "user-portal-url": "https://login.capport.android.com",
| "venue-info-url": "https://venueinfo.capport.android.com"
|}
- """.trimMargin()))
+ """.trimMargin()
+ ))
// Tests will fail if a non-mocked query is received: mock the HTTPS probe, but not the
// HTTP probe as it should not be sent.
@@ -398,8 +437,10 @@
BpfUtils.BPF_CGROUP_INET_EGRESS,
BpfUtils.BPF_CGROUP_INET_SOCK_CREATE
).forEach {
- val ret = SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
- "cmd connectivity bpf-get-cgroup-program-id $it").trim()
+ val ret = SystemUtil.runShellCommand(
+ InstrumentationRegistry.getInstrumentation(),
+ "cmd connectivity bpf-get-cgroup-program-id $it"
+ ).trim()
assertTrue(Integer.parseInt(ret) > 0, "Unexpected output $ret for type $it")
}
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index caf1765..1d2e8b0 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -109,6 +109,7 @@
import com.android.net.module.util.bpf.IngressDiscardKey;
import com.android.net.module.util.bpf.IngressDiscardValue;
import com.android.net.module.util.bpf.LocalNetAccessKey;
+import com.android.server.connectivity.InterfaceTracker;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -170,6 +171,8 @@
@Mock INetd mNetd;
@Mock BpfNetMaps.Dependencies mDeps;
@Mock Context mContext;
+
+ @Mock InterfaceTracker mInterfaceTracker;
private final IBpfMap<S32, U32> mConfigurationMap = new TestBpfMap<>(S32.class, U32.class);
private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap =
new TestBpfMap<>(S32.class, UidOwnerValue.class);
@@ -188,6 +191,7 @@
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
doReturn(TEST_IF_INDEX).when(mDeps).getIfIndex(TEST_IF_NAME);
+ doReturn(TEST_IF_INDEX).when(mInterfaceTracker).getInterfaceIndex(TEST_IF_NAME);
doReturn(TEST_IF_NAME).when(mDeps).getIfName(TEST_IF_INDEX);
doReturn(0).when(mDeps).synchronizeKernelRCU();
BpfNetMaps.setConfigurationMapForTest(mConfigurationMap);
@@ -202,7 +206,7 @@
BpfNetMaps.setDataSaverEnabledMapForTest(mDataSaverEnabledMap);
mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
BpfNetMaps.setIngressDiscardMapForTest(mIngressDiscardMap);
- mBpfNetMaps = new BpfNetMaps(mContext, mNetd, mDeps);
+ mBpfNetMaps = new BpfNetMaps(mContext, mNetd, mDeps, mInterfaceTracker);
}
@Test
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index c4944b6..c28a0f8 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -413,6 +413,7 @@
import com.android.server.connectivity.ClatCoordinator;
import com.android.server.connectivity.ConnectivityFlags;
import com.android.server.connectivity.ConnectivityResources;
+import com.android.server.connectivity.InterfaceTracker;
import com.android.server.connectivity.KeepaliveTracker;
import com.android.server.connectivity.MultinetworkPolicyTracker;
import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies;
@@ -2262,7 +2263,8 @@
}
@Override
- public BpfNetMaps getBpfNetMaps(Context context, INetd netd) {
+ public BpfNetMaps getBpfNetMaps(Context context, INetd netd,
+ InterfaceTracker interfaceTracker) {
return mBpfNetMaps;
}
diff --git a/tests/unit/java/com/android/server/connectivity/InterfaceTrackerTest.java b/tests/unit/java/com/android/server/connectivity/InterfaceTrackerTest.java
new file mode 100644
index 0000000..8a9ada0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/InterfaceTrackerTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.ConnectivityModuleTest;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@ConnectivityModuleTest
+public class InterfaceTrackerTest {
+ private static final String TAG = "InterfaceTrackerTest";
+ private static final String TEST_IF_NAME = "wlan10";
+ private static final String TEST_INCORRECT_IF_NAME = "wlan20";
+ private static final int TEST_IF_INDEX = 7;
+
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
+ private InterfaceTracker mInterfaceTracker;
+
+ @Mock Context mContext;
+ @Mock InterfaceTracker.Dependencies mDeps;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ doReturn(TEST_IF_INDEX).when(mDeps).getIfIndex(TEST_IF_NAME);
+ mInterfaceTracker = new InterfaceTracker(mContext, mDeps);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddingInterface_InterfaceNameIndexMappingAdded() {
+ mInterfaceTracker.addInterface(TEST_IF_NAME);
+ assertEquals(TEST_IF_INDEX, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddingNullInterface_InterfaceNameIndexMappingNotAdded() {
+ mInterfaceTracker.addInterface(null);
+ assertEquals(0, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddingIncorrectInterface_InterfaceNameIndexMappingNotAdded() {
+ mInterfaceTracker.addInterface(TEST_INCORRECT_IF_NAME);
+
+ assertEquals(0, mInterfaceTracker.getInterfaceIndex(TEST_INCORRECT_IF_NAME));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemovingInterface_InterfaceNameIndexMappingRemoved() {
+ mInterfaceTracker.addInterface(TEST_IF_NAME);
+ assertEquals(TEST_IF_INDEX, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+ mInterfaceTracker.removeInterface(TEST_IF_NAME);
+ assertEquals(0, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemovingNullInterface_InterfaceNameIndexMappingNotRemoved() {
+ mInterfaceTracker.addInterface(TEST_IF_NAME);
+ mInterfaceTracker.removeInterface(null);
+ assertEquals(TEST_IF_INDEX, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemovingIncorrectInterface_InterfaceNameIndexMappingNotRemoved() {
+ mInterfaceTracker.addInterface(TEST_IF_NAME);
+ mInterfaceTracker.removeInterface(TEST_INCORRECT_IF_NAME);
+ assertEquals(TEST_IF_INDEX, mInterfaceTracker.getInterfaceIndex(TEST_IF_NAME));
+ }
+
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/DiscoveryExecutorTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/DiscoveryExecutorTest.kt
index 51539a0..67fb428 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/DiscoveryExecutorTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/DiscoveryExecutorTest.kt
@@ -17,6 +17,7 @@
package com.android.server.connectivity.mdns
import android.os.Build
+import android.os.Handler
import android.os.HandlerThread
import android.testing.TestableLooper
import com.android.testutils.DevSdkIgnoreRule
@@ -36,6 +37,8 @@
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class DiscoveryExecutorTest {
private val thread = HandlerThread(DiscoveryExecutorTest::class.simpleName).apply { start() }
+ private val handler by lazy { Handler(thread.looper) }
+ private val testableLooper by lazy { TestableLooper(thread.looper) }
@After
fun tearDown() {
@@ -45,8 +48,10 @@
@Test
fun testCheckAndRunOnHandlerThread() {
- val testableLooper = TestableLooper(thread.looper)
- val executor = DiscoveryExecutor(testableLooper.looper)
+ val executor = DiscoveryExecutor(
+ testableLooper.looper,
+ MdnsFeatureFlags.newBuilder().build()
+ )
try {
val future = CompletableFuture<Boolean>()
executor.checkAndRunOnHandlerThread { future.complete(true) }
@@ -58,17 +63,17 @@
// Create a DiscoveryExecutor with the null defaultLooper and verify the task can execute
// normally.
- val executor2 = DiscoveryExecutor(null /* defaultLooper */)
+ val executor2 = DiscoveryExecutor(
+ null /* defaultLooper */,
+ MdnsFeatureFlags.newBuilder().build()
+ )
val future2 = CompletableFuture<Boolean>()
executor2.checkAndRunOnHandlerThread { future2.complete(true) }
assertTrue(future2.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS))
executor2.shutDown()
}
- @Test
- fun testExecute() {
- val testableLooper = TestableLooper(thread.looper)
- val executor = DiscoveryExecutor(testableLooper.looper)
+ private fun verifyExecute(executor: DiscoveryExecutor) {
try {
val future = CompletableFuture<Boolean>()
executor.execute { future.complete(true) }
@@ -81,9 +86,27 @@
}
@Test
+ fun testExecute() {
+ verifyExecute(DiscoveryExecutor(
+ testableLooper.looper,
+ MdnsFeatureFlags.newBuilder().build()
+ ))
+ }
+
+ @Test
+ fun testExecute_RealtimeScheduler() {
+ verifyExecute(DiscoveryExecutor(
+ testableLooper.looper,
+ MdnsFeatureFlags.newBuilder().setIsAccurateDelayCallbackEnabled(true).build()
+ ))
+ }
+
+ @Test
fun testExecuteDelayed() {
- val testableLooper = TestableLooper(thread.looper)
- val executor = DiscoveryExecutor(testableLooper.looper)
+ val executor = DiscoveryExecutor(
+ testableLooper.looper,
+ MdnsFeatureFlags.newBuilder().build()
+ )
try {
// Verify the executeDelayed method
val future = CompletableFuture<Boolean>()
@@ -107,4 +130,21 @@
testableLooper.destroy()
}
}
+
+ @Test
+ fun testExecuteDelayed_RealtimeScheduler() {
+ val executor = DiscoveryExecutor(
+ thread.looper,
+ MdnsFeatureFlags.newBuilder().setIsAccurateDelayCallbackEnabled(true).build()
+ )
+ try {
+ // Verify the executeDelayed method
+ val future = CompletableFuture<Boolean>()
+ // Schedule a task with 50ms delay
+ executor.executeDelayed({ future.complete(true) }, 50L)
+ assertTrue(future.get(500L, TimeUnit.MILLISECONDS))
+ } finally {
+ testableLooper.destroy()
+ }
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index dad03e0..b9c0d2f 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -59,7 +59,6 @@
import android.text.TextUtils;
import com.android.net.module.util.CollectionUtils;
-import com.android.net.module.util.RealtimeScheduler;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -129,7 +128,7 @@
@Mock
private MdnsServiceTypeClient.Dependencies mockDeps;
@Mock
- private RealtimeScheduler mockRealtimeScheduler;
+ private Scheduler mockScheduler;
@Captor
private ArgumentCaptor<MdnsServiceInfo> serviceInfoCaptor;
@@ -250,14 +249,14 @@
doAnswer(inv -> {
realHandler = (Handler) inv.getArguments()[0];
- return mockRealtimeScheduler;
- }).when(mockDeps).createRealtimeScheduler(any(Handler.class));
+ return mockScheduler;
+ }).when(mockDeps).createScheduler(any(Handler.class));
doAnswer(inv -> {
message = (Message) inv.getArguments()[0];
latestDelayMs = (long) inv.getArguments()[1];
return null;
- }).when(mockRealtimeScheduler).sendDelayedMessage(any(), anyLong());
+ }).when(mockScheduler).sendDelayedMessage(any(), anyLong());
client = makeMdnsServiceTypeClient(featureFlags);
}
@@ -2137,7 +2136,7 @@
.setNumOfQueriesBeforeBackoff(numOfQueriesBeforeBackoff)
.build();
startSendAndReceive(mockListenerOne, searchOptions);
- verify(mockRealtimeScheduler, times(1)).removeDelayedMessage(EVENT_START_QUERYTASK);
+ verify(mockScheduler, times(1)).removeDelayedMessage(EVENT_START_QUERYTASK);
// Verify that the first query has been sent.
verifyAndSendQuery(0 /* index */, 0 /* timeInMs */, true /* expectsUnicastResponse */,
@@ -2159,13 +2158,13 @@
// 0.8 * smallestRemainingTtl is larger than time to next run.
long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
- doReturn(true).when(mockRealtimeScheduler).hasDelayedMessage(EVENT_START_QUERYTASK);
+ doReturn(true).when(mockScheduler).hasDelayedMessage(EVENT_START_QUERYTASK);
processResponse(createResponse(
"service-instance-1", "192.0.2.123", 5353,
SERVICE_TYPE_LABELS,
Collections.emptyMap(), TEST_TTL), socketKey);
// Verify that the message removal occurred.
- verify(mockRealtimeScheduler, times(6)).removeDelayedMessage(EVENT_START_QUERYTASK);
+ verify(mockScheduler, times(6)).removeDelayedMessage(EVENT_START_QUERYTASK);
assertNotNull(message);
verifyAndSendQuery(3 /* index */, (long) (TEST_TTL / 2 * 0.8) /* timeInMs */,
true /* expectsUnicastResponse */, true /* multipleSocketDiscovery */,
@@ -2174,7 +2173,7 @@
// Stop sending packets.
stopSendAndReceive(mockListenerOne);
- verify(mockRealtimeScheduler, times(8)).removeDelayedMessage(EVENT_START_QUERYTASK);
+ verify(mockScheduler, times(8)).removeDelayedMessage(EVENT_START_QUERYTASK);
}
@Test
@@ -2184,12 +2183,12 @@
// Start query
startSendAndReceive(mockListenerOne, MdnsSearchOptions.newBuilder().build());
- verify(mockRealtimeScheduler, times(1)).removeDelayedMessage(EVENT_START_QUERYTASK);
+ verify(mockScheduler, times(1)).removeDelayedMessage(EVENT_START_QUERYTASK);
// Stop query and verify the close() method has been called.
stopSendAndReceive(mockListenerOne);
- verify(mockRealtimeScheduler, times(2)).removeDelayedMessage(EVENT_START_QUERYTASK);
- verify(mockRealtimeScheduler).close();
+ verify(mockScheduler, times(2)).removeDelayedMessage(EVENT_START_QUERYTASK);
+ verify(mockScheduler).close();
}
private static MdnsServiceInfo matchServiceName(String name) {
@@ -2247,8 +2246,7 @@
.sendMessage(any(Handler.class), any(Message.class));
// Verify the task has been scheduled.
if (useAccurateDelayCallback) {
- verify(mockRealtimeScheduler, times(scheduledCount))
- .sendDelayedMessage(any(), anyLong());
+ verify(mockScheduler, times(scheduledCount)).sendDelayedMessage(any(), anyLong());
} else {
verify(mockDeps, times(scheduledCount))
.sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 557bfd6..d7e781e 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -68,6 +68,7 @@
import com.android.server.connectivity.CarrierPrivilegeAuthenticator
import com.android.server.connectivity.ClatCoordinator
import com.android.server.connectivity.ConnectivityFlags
+import com.android.server.connectivity.InterfaceTracker
import com.android.server.connectivity.MulticastRoutingCoordinatorService
import com.android.server.connectivity.MultinetworkPolicyTracker
import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
@@ -193,6 +194,7 @@
val connResources = makeMockConnResources(sysResources, packageManager)
val netd = mock<INetd>()
+ val interfaceTracker = mock<InterfaceTracker>()
val bpfNetMaps = mock<BpfNetMaps>().also {
doReturn(PERMISSION_INTERNET).`when`(it).getNetPermForUid(anyInt())
}
@@ -279,7 +281,11 @@
inner class CSDeps : ConnectivityService.Dependencies() {
override fun getResources(ctx: Context) = connResources
- override fun getBpfNetMaps(context: Context, netd: INetd) = this@CSTest.bpfNetMaps
+ override fun getBpfNetMaps(
+ context: Context,
+ netd: INetd,
+ interfaceTracker: InterfaceTracker
+ ) = this@CSTest.bpfNetMaps
override fun getClatCoordinator(netd: INetd?) = this@CSTest.clatCoordinator
override fun getNetworkStack() = this@CSTest.networkStack
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 697bf9e..c5929f1 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -65,7 +65,6 @@
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
@@ -96,6 +95,7 @@
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
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/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 73a6bda..b649716 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -227,8 +227,8 @@
* specific error:
*
* <ul>
- * <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} when this device is not
- * attached to Thread network
+ * <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} when this device is not a
+ * Border Router or not attached to Thread network
* <li>{@link ThreadNetworkException#ERROR_BUSY} when ephemeral key mode is already activated
* on the device, caller can recover from this error when the ephemeral key mode gets
* deactivated
@@ -267,7 +267,8 @@
* connection will be terminated.
*
* <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called. The call will
- * always succeed if the device is not in ephemeral key mode.
+ * always succeed if the device is not in ephemeral key mode. It returns an error {@link
+ * ThreadNetworkException#ERROR_FAILED_PRECONDITION} if this device is not a Border Router.
*
* @param executor the executor to execute {@code receiver}
* @param receiver the receiver to receive the result of this operation
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index af16d19..7063357 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -559,7 +559,7 @@
// The persistent setting keeps the desired enabled state, thus it's set regardless
// the otDaemon set enabled state operation succeeded or not, so that it can recover
// to the desired value after reboot.
- mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
+ mPersistentSettings.put(ThreadPersistentSettings.KEY_THREAD_ENABLED, isEnabled);
}
try {
@@ -743,7 +743,7 @@
private boolean shouldEnableThread() {
return !mForceStopOtDaemonEnabled
&& !mUserRestricted
- && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+ && mPersistentSettings.get(ThreadPersistentSettings.KEY_THREAD_ENABLED);
}
private void requestUpstreamNetwork() {
@@ -879,10 +879,8 @@
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
final var scoreBuilder = new NetworkScore.Builder();
- if (isBorderRouterMode()) {
- netCapsBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK);
- scoreBuilder.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK);
- }
+ netCapsBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK);
+ scoreBuilder.setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK);
return new NetworkAgent(
mContext,
@@ -890,7 +888,7 @@
LOG.getTag(),
netCapsBuilder.build(),
getTunIfLinkProperties(),
- isBorderRouterMode() ? newLocalNetworkConfig() : null,
+ newLocalNetworkConfig(),
scoreBuilder.build(),
new NetworkAgentConfig.Builder().build(),
mNetworkProvider) {
@@ -899,9 +897,8 @@
@Override
public void onNetworkUnwanted() {
LOG.i("Thread network is unwanted by ConnectivityService");
- if (!isBorderRouterMode()) {
- leave(false /* eraseDataset */, new LoggingOperationReceiver("leave"));
- }
+ // TODO(b/374037595): leave() the current network when the new APIs for mobile
+ // is available
}
};
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
index 2cd34e8..ff0e2c1 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
@@ -16,7 +16,7 @@
package com.android.server.thread;
-import static com.android.server.thread.ThreadPersistentSettings.THREAD_COUNTRY_CODE;
+import static com.android.server.thread.ThreadPersistentSettings.KEY_COUNTRY_CODE;
import android.annotation.Nullable;
import android.annotation.StringDef;
@@ -496,7 +496,7 @@
return mLocationCountryCodeInfo;
}
- String settingsCountryCode = mPersistentSettings.get(THREAD_COUNTRY_CODE);
+ String settingsCountryCode = mPersistentSettings.get(KEY_COUNTRY_CODE);
if (settingsCountryCode != null) {
return new CountryCodeInfo(settingsCountryCode, COUNTRY_CODE_SOURCE_SETTINGS);
}
@@ -514,8 +514,7 @@
public void onSuccess() {
synchronized ("ThreadNetworkCountryCode.this") {
mCurrentCountryCodeInfo = countryCodeInfo;
- mPersistentSettings.put(
- THREAD_COUNTRY_CODE.key, countryCodeInfo.getCountryCode());
+ mPersistentSettings.put(KEY_COUNTRY_CODE, countryCodeInfo.getCountryCode());
}
}
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index 746b587..fd6dec7 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -40,6 +40,8 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
/**
* Store persistent data for Thread network settings. These are key (string) / value pairs that are
@@ -53,54 +55,55 @@
/** File name used for storing settings. */
private static final String FILE_NAME = "ThreadPersistentSettings.xml";
- /** Current config store data version. This will be incremented for any additions. */
+ /** Current config store data version. This MUST be incremented for any incompatible changes. */
private static final int CURRENT_SETTINGS_STORE_DATA_VERSION = 1;
/**
* Stores the version of the data. This can be used to handle migration of data if some
* non-backward compatible change introduced.
*/
- private static final String VERSION_KEY = "version";
+ private static final String KEY_VERSION = "version";
- /******** Thread persistent setting keys ***************/
- /** Stores the Thread feature toggle state, true for enabled and false for disabled. */
- public static final Key<Boolean> THREAD_ENABLED = new Key<>("thread_enabled", true);
+ /**
+ * Saves the boolean flag for Thread being enabled. The value defaults to resource overlay value
+ * {@code R.bool.config_thread_default_enabled}.
+ */
+ public static final Key<Boolean> KEY_THREAD_ENABLED = new Key<>("thread_enabled");
+
+ /**
+ * Saves the boolean flag for border router being enabled. The value defaults to resource
+ * overlay value {@code R.bool.config_thread_border_router_default_enabled}.
+ */
+ private static final Key<Boolean> KEY_CONFIG_BORDER_ROUTER_ENABLED =
+ new Key<>("config_border_router_enabled");
+
+ /** Stores the Thread NAT64 feature toggle state, true for enabled and false for disabled. */
+ private static final Key<Boolean> KEY_CONFIG_NAT64_ENABLED = new Key<>("config_nat64_enabled");
+
+ /**
+ * Stores the Thread DHCPv6-PD feature toggle state, true for enabled and false for disabled.
+ */
+ private static final Key<Boolean> KEY_CONFIG_DHCP6_PD_ENABLED =
+ new Key<>("config_dhcp6_pd_enabled");
/**
* Indicates that Thread was enabled (i.e. via the setEnabled() API) when the airplane mode is
* turned on in settings. When this value is {@code true}, the current airplane mode state will
* be ignored when evaluating the Thread enabled state.
*/
- public static final Key<Boolean> THREAD_ENABLED_IN_AIRPLANE_MODE =
- new Key<>("thread_enabled_in_airplane_mode", false);
+ public static final Key<Boolean> KEY_THREAD_ENABLED_IN_AIRPLANE_MODE =
+ new Key<>("thread_enabled_in_airplane_mode");
/** Stores the Thread country code, null if no country code is stored. */
- public static final Key<String> THREAD_COUNTRY_CODE = new Key<>("thread_country_code", null);
-
- /**
- * Saves the boolean flag for border router being enabled. The value defaults to {@code true} if
- * this config is missing.
- */
- private static final Key<Boolean> CONFIG_BORDER_ROUTER_ENABLED =
- new Key<>("config_border_router_enabled", true);
-
- /** Stores the Thread NAT64 feature toggle state, true for enabled and false for disabled. */
- private static final Key<Boolean> CONFIG_NAT64_ENABLED =
- new Key<>("config_nat64_enabled", false);
-
- /**
- * Stores the Thread DHCPv6-PD feature toggle state, true for enabled and false for disabled.
- */
- private static final Key<Boolean> CONFIG_DHCP6_PD_ENABLED =
- new Key<>("config_dhcp6_pd_enabled", false);
-
- /******** Thread persistent setting keys ***************/
+ public static final Key<String> KEY_COUNTRY_CODE = new Key<>("thread_country_code");
@GuardedBy("mLock")
private final AtomicFile mAtomicFile;
private final Object mLock = new Object();
+ private final Map<String, Object> mDefaultValues = new HashMap<>();
+
@GuardedBy("mLock")
private final PersistableBundle mSettings = new PersistableBundle();
@@ -116,19 +119,22 @@
ThreadPersistentSettings(AtomicFile atomicFile, ConnectivityResources resources) {
mAtomicFile = atomicFile;
mResources = resources;
+
+ mDefaultValues.put(
+ KEY_THREAD_ENABLED.key,
+ mResources.get().getBoolean(R.bool.config_thread_default_enabled));
+ mDefaultValues.put(
+ KEY_CONFIG_BORDER_ROUTER_ENABLED.key,
+ mResources.get().getBoolean(R.bool.config_thread_border_router_default_enabled));
+ mDefaultValues.put(KEY_CONFIG_NAT64_ENABLED.key, false);
+ mDefaultValues.put(KEY_CONFIG_DHCP6_PD_ENABLED.key, false);
+ mDefaultValues.put(KEY_THREAD_ENABLED_IN_AIRPLANE_MODE.key, false);
+ mDefaultValues.put(KEY_COUNTRY_CODE.key, null);
}
/** Initialize the settings by reading from the settings file. */
public void initialize() {
readFromStoreFile();
- synchronized (mLock) {
- if (!mSettings.containsKey(THREAD_ENABLED.key)) {
- LOG.i("\"thread_enabled\" is missing in settings file, using default value");
- put(
- THREAD_ENABLED.key,
- mResources.get().getBoolean(R.bool.config_thread_default_enabled));
- }
- }
}
private void putObject(String key, @Nullable Object value) {
@@ -173,25 +179,17 @@
return (T) value;
}
- /**
- * Store a value to the stored settings.
- *
- * @param key One of the settings keys.
- * @param value Value to be stored.
- */
- public <T> void put(String key, @Nullable T value) {
- putObject(key, value);
+ /** Stores a value to the stored settings. */
+ public <T> void put(Key<T> key, @Nullable T value) {
+ putObject(key.key, value);
writeToStoreFile();
}
- /**
- * Retrieve a value from the stored settings.
- *
- * @param key One of the settings keys.
- * @return value stored in settings, defValue if the key does not exist.
- */
+ /** Retrieves a value from the stored settings. */
+ @Nullable
public <T> T get(Key<T> key) {
- return getObject(key.key, key.defaultValue);
+ T defaultValue = (T) mDefaultValues.get(key.key);
+ return getObject(key.key, defaultValue);
}
/**
@@ -204,9 +202,9 @@
if (getConfiguration().equals(configuration)) {
return false;
}
- putObject(CONFIG_BORDER_ROUTER_ENABLED.key, configuration.isBorderRouterEnabled());
- putObject(CONFIG_NAT64_ENABLED.key, configuration.isNat64Enabled());
- putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcpv6PdEnabled());
+ put(KEY_CONFIG_BORDER_ROUTER_ENABLED, configuration.isBorderRouterEnabled());
+ put(KEY_CONFIG_NAT64_ENABLED, configuration.isNat64Enabled());
+ put(KEY_CONFIG_DHCP6_PD_ENABLED, configuration.isDhcpv6PdEnabled());
writeToStoreFile();
return true;
}
@@ -214,9 +212,9 @@
/** Retrieve the {@link ThreadConfiguration} from the persistent settings. */
public ThreadConfiguration getConfiguration() {
return new ThreadConfiguration.Builder()
- .setBorderRouterEnabled(get(CONFIG_BORDER_ROUTER_ENABLED))
- .setNat64Enabled(get(CONFIG_NAT64_ENABLED))
- .setDhcpv6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
+ .setBorderRouterEnabled(get(KEY_CONFIG_BORDER_ROUTER_ENABLED))
+ .setNat64Enabled(get(KEY_CONFIG_NAT64_ENABLED))
+ .setDhcpv6PdEnabled(get(KEY_CONFIG_DHCP6_PD_ENABLED))
.build();
}
@@ -225,18 +223,11 @@
*
* @param <T> Type of the value.
*/
- public static class Key<T> {
- public final String key;
- public final T defaultValue;
+ public static final class Key<T> {
+ @VisibleForTesting final String key;
- private Key(String key, T defaultValue) {
+ private Key(String key) {
this.key = key;
- this.defaultValue = defaultValue;
- }
-
- @Override
- public String toString() {
- return "[Key: " + key + ", DefaultValue: " + defaultValue + "]";
}
}
@@ -247,7 +238,7 @@
synchronized (mLock) {
bundleToWrite = new PersistableBundle(mSettings);
}
- bundleToWrite.putInt(VERSION_KEY, CURRENT_SETTINGS_STORE_DATA_VERSION);
+ bundleToWrite.putInt(KEY_VERSION, CURRENT_SETTINGS_STORE_DATA_VERSION);
bundleToWrite.writeToStream(outputStream);
synchronized (mLock) {
writeToAtomicFile(mAtomicFile, outputStream.toByteArray());
@@ -267,7 +258,7 @@
final ByteArrayInputStream inputStream = new ByteArrayInputStream(readData);
final PersistableBundle bundleRead = PersistableBundle.readFromStream(inputStream);
// Version unused for now. May be needed in the future for handling migrations.
- bundleRead.remove(VERSION_KEY);
+ bundleRead.remove(KEY_VERSION);
synchronized (mLock) {
mSettings.putAll(bundleRead);
}
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index 520a434..2f2a5d1 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -326,12 +326,13 @@
private static LinkAddress newLinkAddress(
Ipv6AddressInfo addressInfo, boolean hasActiveOmrAddress) {
// Mesh-local addresses and OMR address have the same scope, to distinguish them we set
- // mesh-local addresses as deprecated when there is an active OMR address.
+ // mesh-local addresses as deprecated when there is an active OMR address. If OMR address
+ // is missing, only ML-EID in mesh-local addresses will be set preferred.
// For OMR address and link-local address we only use the value isPreferred set by
// ot-daemon.
boolean isPreferred = addressInfo.isPreferred;
- if (addressInfo.isMeshLocal && hasActiveOmrAddress) {
- isPreferred = false;
+ if (addressInfo.isMeshLocal) {
+ isPreferred = (!hasActiveOmrAddress && addressInfo.isMeshLocalEid);
}
final long deprecationTimeMillis =
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index a979721..2d68119 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -49,6 +49,8 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -94,12 +96,15 @@
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -118,6 +123,7 @@
/** CTS tests for {@link ThreadNetworkController}. */
@LargeTest
@RequiresThreadFeature
+@RunWith(Parameterized.class)
public class ThreadNetworkControllerTest {
private static final int JOIN_TIMEOUT_MILLIS = 30 * 1000;
private static final int LEAVE_TIMEOUT_MILLIS = 2_000;
@@ -134,8 +140,6 @@
private static final String MESHCOP_SERVICE_TYPE = "_meshcop._udp";
private static final String THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
- private static final ThreadConfiguration DEFAULT_CONFIG =
- new ThreadConfiguration.Builder().build();
private static final SparseIntArray CHANNEL_MAX_POWERS =
new SparseIntArray() {
{
@@ -161,6 +165,22 @@
private final List<Consumer<ThreadConfiguration>> mConfigurationCallbacksToCleanUp =
new ArrayList<>();
+ public final boolean mIsBorderRouterEnabled;
+ private final ThreadConfiguration mDefaultConfig;
+
+ @Parameterized.Parameters
+ public static Collection configArguments() {
+ return Arrays.asList(new Object[][] {{false}, {true}});
+ }
+
+ public ThreadNetworkControllerTest(boolean isBorderRouterEnabled) {
+ mIsBorderRouterEnabled = isBorderRouterEnabled;
+ mDefaultConfig =
+ new ThreadConfiguration.Builder()
+ .setBorderRouterEnabled(isBorderRouterEnabled)
+ .build();
+ }
+
@Before
public void setUp() throws Exception {
mController =
@@ -175,8 +195,10 @@
mHandlerThread.start();
setEnabledAndWait(mController, true);
- setConfigurationAndWait(mController, DEFAULT_CONFIG);
- deactivateEphemeralKeyModeAndWait(mController);
+ setConfigurationAndWait(mController, mDefaultConfig);
+ if (mDefaultConfig.isBorderRouterEnabled()) {
+ deactivateEphemeralKeyModeAndWait(mController);
+ }
}
@After
@@ -185,7 +207,7 @@
setEnabledAndWait(mController, true);
leaveAndWait(mController);
tearDownTestNetwork();
- setConfigurationAndWait(mController, DEFAULT_CONFIG);
+ setConfigurationAndWait(mController, mDefaultConfig);
for (Consumer<ThreadConfiguration> configurationCallback :
mConfigurationCallbacksToCleanUp) {
try {
@@ -197,7 +219,9 @@
}
}
mConfigurationCallbacksToCleanUp.clear();
- deactivateEphemeralKeyModeAndWait(mController);
+ if (mDefaultConfig.isBorderRouterEnabled()) {
+ deactivateEphemeralKeyModeAndWait(mController);
+ }
}
@Test
@@ -573,7 +597,7 @@
@Override
public void onActiveOperationalDatasetChanged(
ActiveOperationalDataset activeDataset) {
- if (activeDataset.equals(activeDataset2)) {
+ if (Objects.equals(activeDataset, activeDataset2)) {
dataset2IsApplied.complete(true);
}
}
@@ -843,6 +867,7 @@
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_withPrivilegedPermission_succeeds() throws Exception {
+ assumeTrue(mDefaultConfig.isBorderRouterEnabled());
joinRandomizedDatasetAndWait(mController);
CompletableFuture<Void> startFuture = new CompletableFuture<>();
@@ -861,6 +886,7 @@
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
throws Exception {
+ assumeTrue(mDefaultConfig.isBorderRouterEnabled());
dropAllPermissions();
assertThrows(
@@ -874,6 +900,7 @@
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_withZeroLifetime_throwsIllegalArgumentException()
throws Exception {
+ assumeTrue(mDefaultConfig.isBorderRouterEnabled());
grantPermissions(THREAD_NETWORK_PRIVILEGED);
assertThrows(
@@ -885,6 +912,7 @@
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_withInvalidLargeLifetime_throwsIllegalArgumentException()
throws Exception {
+ assumeTrue(mDefaultConfig.isBorderRouterEnabled());
grantPermissions(THREAD_NETWORK_PRIVILEGED);
Duration lifetime = mController.getMaxEphemeralKeyLifetime().plusMillis(1);
@@ -897,6 +925,7 @@
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void activateEphemeralKeyMode_concurrentRequests_secondOneFailsWithBusyError()
throws Exception {
+ assumeTrue(mDefaultConfig.isBorderRouterEnabled());
joinRandomizedDatasetAndWait(mController);
CompletableFuture<Void> future1 = new CompletableFuture<>();
CompletableFuture<Void> future2 = new CompletableFuture<>();
@@ -945,6 +974,7 @@
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void deactivateEphemeralKeyMode_withoutPrivilegedPermission_throwsSecurityException()
throws Exception {
+ assumeTrue(mDefaultConfig.isBorderRouterEnabled());
dropAllPermissions();
assertThrows(
@@ -956,9 +986,7 @@
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void deactivateEphemeralKeyMode_notBorderRouter_failsWithFailedPrecondition()
throws Exception {
- setConfigurationAndWait(
- mController,
- new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build());
+ assumeFalse(mDefaultConfig.isBorderRouterEnabled());
grantPermissions(THREAD_NETWORK_PRIVILEGED);
CompletableFuture<Void> future = new CompletableFuture<>();
@@ -975,6 +1003,7 @@
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_permissionsGranted_returnsCurrentState() throws Exception {
+ assumeTrue(mDefaultConfig.isBorderRouterEnabled());
CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
@@ -1011,6 +1040,7 @@
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_withoutThreadPriviledgedPermission_returnsNullEphemeralKey()
throws Exception {
+ assumeTrue(mDefaultConfig.isBorderRouterEnabled());
CompletableFuture<Integer> stateFuture = new CompletableFuture<>();
CompletableFuture<String> ephemeralKeyFuture = new CompletableFuture<>();
CompletableFuture<Instant> expiryFuture = new CompletableFuture<>();
@@ -1050,6 +1080,7 @@
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_ephemralKeyStateChanged_returnsUpdatedState() throws Exception {
+ assumeTrue(mDefaultConfig.isBorderRouterEnabled());
EphemeralKeyStateListener listener = new EphemeralKeyStateListener(mController);
joinRandomizedDatasetAndWait(mController);
@@ -1068,6 +1099,7 @@
@Test
@RequiresFlagsEnabled({Flags.FLAG_EPSKC_ENABLED})
public void subscribeEpskcState_epskcEnabled_returnsSameExpiry() throws Exception {
+ assumeTrue(mDefaultConfig.isBorderRouterEnabled());
EphemeralKeyStateListener listener1 = new EphemeralKeyStateListener(mController);
Triple<Integer, String, Instant> epskc1;
try {
@@ -1173,7 +1205,7 @@
THREAD_NETWORK_PRIVILEGED,
() -> registerConfigurationCallback(mController, mExecutor, callback));
assertThat(getConfigFuture.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS))
- .isEqualTo(DEFAULT_CONFIG);
+ .isEqualTo(mDefaultConfig);
}
@Test
@@ -1216,7 +1248,7 @@
setFuture1.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
setFuture2.get(ENABLED_TIMEOUT_MILLIS, MILLISECONDS);
- listener.expectConfiguration(DEFAULT_CONFIG);
+ listener.expectConfiguration(mDefaultConfig);
listener.expectConfiguration(config1);
listener.expectConfiguration(config2);
listener.expectNoMoreConfiguration();
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
index b6d0d31..5be8f49 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
@@ -19,12 +19,10 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeFalse;
-import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.assumeTrue;
import android.content.Context;
import android.content.pm.PackageManager;
-import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkManager;
import android.os.Build;
@@ -41,8 +39,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.util.List;
-
/** Tests for {@link ThreadNetworkManager}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -90,21 +86,4 @@
assertThat(mManager).isNotNull();
}
-
- @Test
- public void getManager_noThreadFeature_returnsNull() {
- assumeFalse(mPackageManager.hasSystemFeature("android.hardware.thread_network"));
-
- assertThat(mManager).isNull();
- }
-
- @Test
- @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
- public void getAllThreadNetworkControllers_managerIsNotNull_returnsNotEmptyList() {
- assumeNotNull(mManager);
-
- List<ThreadNetworkController> controllers = mManager.getAllThreadNetworkControllers();
-
- assertThat(controllers).isNotEmpty();
- }
}
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 875a4ad..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,12 +14,12 @@
* 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;
import static android.net.thread.utils.IntegrationTestUtils.buildIcmpv4EchoReply;
-import static android.net.thread.utils.IntegrationTestUtils.enableThreadAndJoinNetwork;
+import static android.net.thread.utils.IntegrationTestUtils.enableBorderRouterAndJoinNetwork;
import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv4Packet;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
@@ -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;
@@ -128,7 +128,7 @@
@BeforeClass
public static void beforeClass() throws Exception {
- enableThreadAndJoinNetwork(DEFAULT_DATASET);
+ enableBorderRouterAndJoinNetwork(DEFAULT_DATASET);
}
@AfterClass
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/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
index f959ccf..c4e373a 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -21,6 +21,7 @@
import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.discoverForServiceLost;
import static android.net.thread.utils.IntegrationTestUtils.discoverService;
+import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWait;
import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr;
import static android.net.thread.utils.IntegrationTestUtils.resolveService;
import static android.net.thread.utils.IntegrationTestUtils.resolveServiceUntil;
@@ -115,6 +116,8 @@
public void setUp() throws Exception {
mController.setEnabledAndWait(true);
mController.leaveAndWait();
+ var config = new ThreadConfiguration.Builder().setBorderRouterEnabled(true).build();
+ mController.setConfigurationAndWait(config);
mController.joinAndWait(DEFAULT_DATASET);
mNsdManager = mContext.getSystemService(NsdManager.class);
@@ -158,6 +161,143 @@
}
@Test
+ public void advertisingProxy_borderRouterDisabled_clientServiceRemovedWhenLeaveIsCalled()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * SRP Server / AD Proxy -------------- SRP Client
+ *
+ * </pre>
+ */
+
+ // The Border Router / SRP Server mode can only be changed when Thread is disconnected
+ mController.leaveAndWait();
+ var config = new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build();
+ mController.setConfigurationAndWait(config);
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ FullThreadDevice srpClient = mFtds.get(0);
+ joinNetworkAndWait(srpClient, DEFAULT_DATASET);
+ srpClient.setSrpHostname("thread-srp-client-host");
+ srpClient.setSrpHostAddresses(List.of(srpClient.getMlEid()));
+ srpClient.addSrpService(
+ "thread-srp-client-service",
+ "_matter._tcp",
+ List.of("_sub1", "_sub2"),
+ 12345 /* port */,
+ Map.of("key1", bytes(1), "key2", bytes(2)));
+ NsdServiceInfo discoveredService = discoverService(mNsdManager, "_matter._tcp");
+ assertThat(discoveredService).isNotNull();
+
+ CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
+ NsdManager.DiscoveryListener listener =
+ discoverForServiceLost(mNsdManager, "_matter._tcp", serviceLostFuture);
+ mController.leaveAndWait();
+
+ // Verify the service becomes lost.
+ try {
+ serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+ } finally {
+ mNsdManager.stopServiceDiscovery(listener);
+ }
+ assertThrows(TimeoutException.class, () -> discoverService(mNsdManager, "_matter._tcp"));
+ }
+
+ @Test
+ public void advertisingProxy_borderRouterDisabled_clientServiceRemovedWhen2ndSrpServerEnabled()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * SRP Server / AD Proxy -------------- SRP Client
+ * (Cuttlefish) |
+ * +------- 2nd SRP Server
+ *
+ * </pre>
+ */
+
+ // The Border Router / SRP Server mode can only be changed when Thread is disconnected
+ mController.leaveAndWait();
+ var config = new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build();
+ mController.setConfigurationAndWait(config);
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ FullThreadDevice srpClient = mFtds.get(0);
+ joinNetworkAndWait(srpClient, DEFAULT_DATASET);
+ srpClient.setSrpHostname("thread-srp-client-host");
+ srpClient.setSrpHostAddresses(List.of(srpClient.getMlEid()));
+ srpClient.addSrpService(
+ "thread-srp-client-service",
+ "_matter._tcp",
+ List.of("_sub1", "_sub2"),
+ 12345 /* port */,
+ Map.of("key1", bytes(1), "key2", bytes(2)));
+ NsdServiceInfo discoveredService = discoverService(mNsdManager, "_matter._tcp");
+ assertThat(discoveredService).isNotNull();
+
+ FullThreadDevice srpServer2 = mFtds.get(1);
+ joinNetworkAndWait(srpServer2, DEFAULT_DATASET);
+ CompletableFuture<NsdServiceInfo> serviceLostFuture = new CompletableFuture<>();
+ NsdManager.DiscoveryListener listener =
+ discoverForServiceLost(mNsdManager, "_matter._tcp", serviceLostFuture);
+ srpServer2.setSrpServerEnabled(true);
+
+ // Verify the service becomes lost.
+ try {
+ serviceLostFuture.get(SERVICE_DISCOVERY_TIMEOUT.toMillis(), MILLISECONDS);
+ } finally {
+ mNsdManager.stopServiceDiscovery(listener);
+ }
+ assertThrows(TimeoutException.class, () -> discoverService(mNsdManager, "_matter._tcp"));
+ }
+
+ @Test
+ public void advertisingProxy_borderRouterDisabled_clientMleIdAddressIsAdvertised()
+ throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * Thread
+ * SRP Server / AD Proxy -------------- SRP Client
+ * (Cuttlefish)
+ *
+ * </pre>
+ */
+
+ // The Border Router / SRP Server mode can only be changed when Thread is disconnected
+ mController.leaveAndWait();
+ var config = new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build();
+ mController.setConfigurationAndWait(config);
+ mController.joinAndWait(DEFAULT_DATASET);
+
+ FullThreadDevice srpClient = mFtds.getFirst();
+ joinNetworkAndWait(srpClient, DEFAULT_DATASET);
+ srpClient.setSrpHostname("thread-srp-client-host");
+ srpClient.setSrpHostAddresses(List.of(srpClient.getMlEid()));
+ srpClient.addSrpService(
+ "thread-srp-client-service",
+ "_matter._tcp",
+ List.of("_sub1", "_sub2"),
+ 12345 /* port */,
+ Map.of("key1", bytes(1), "key2", bytes(2)));
+
+ NsdServiceInfo discoveredService = discoverService(mNsdManager, "_matter._tcp");
+ assertThat(discoveredService).isNotNull();
+ NsdServiceInfo resolvedService = resolveService(mNsdManager, discoveredService);
+ assertThat(resolvedService.getServiceName()).isEqualTo("thread-srp-client-service");
+ assertThat(resolvedService.getServiceType()).isEqualTo("_matter._tcp");
+ assertThat(resolvedService.getPort()).isEqualTo(12345);
+ assertThat(resolvedService.getAttributes())
+ .comparingValuesUsing(BYTE_ARRAY_EQUALITY)
+ .containsExactly("key1", bytes(1), "key2", bytes(2));
+ assertThat(resolvedService.getHostname()).isEqualTo("thread-srp-client-host");
+ assertThat(resolvedService.getHostAddresses()).containsExactly(srpClient.getMlEid());
+ }
+
+ @Test
public void advertisingProxy_multipleSrpClientsRegisterServices_servicesResolvableByMdns()
throws Exception {
/*
@@ -455,7 +595,8 @@
DeviceConfigUtils.getDeviceConfigPropertyBoolean(
"thread_network", "TrelFeature__enabled", false));
- NsdServiceInfo discoveredService = discoverService(mNsdManager, "_trel._udp");
+ NsdServiceInfo discoveredService =
+ discoverService(mNsdManager, "_trel._udp", mOtCtl.getExtendedAddr());
assertThat(discoveredService).isNotNull();
// Resolve service with the current TREL port, otherwise it may return stale service from
// a previous infra link setup.
@@ -478,7 +619,9 @@
DeviceConfigUtils.getDeviceConfigPropertyBoolean(
"thread_network", "TrelFeature__enabled", false));
- assertThrows(TimeoutException.class, () -> discoverService(mNsdManager, "_trel._udp"));
+ assertThrows(
+ TimeoutException.class,
+ () -> discoverService(mNsdManager, "_trel._udp", mOtCtl.getExtendedAddr()));
}
private void registerService(NsdServiceInfo serviceInfo, RegistrationListener listener)
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index 0e8f824..b608c5d 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -17,23 +17,18 @@
package android.net.thread;
import static android.Manifest.permission.ACCESS_NETWORK_STATE;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
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.DEFAULT_CONFIG;
-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;
@@ -48,14 +43,13 @@
import android.net.ConnectivityManager;
import android.net.InetAddresses;
import android.net.IpPrefix;
-import android.net.LinkAddress;
-import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.OtDaemonController;
import android.net.thread.utils.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;
@@ -65,6 +59,8 @@
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;
@@ -94,11 +90,11 @@
// 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);
-
private static final Duration NETWORK_CALLBACK_TIMEOUT = Duration.ofSeconds(10);
+ // The duration between attached and addresses show up on thread-wpan
+ private static final Duration LINK_ADDR_TIMEOUT = Duration.ofSeconds(2);
+
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
base16().decode(
@@ -109,11 +105,6 @@
+ "B9D351B40C0402A0FFF8");
private static final ActiveOperationalDataset DEFAULT_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
- private static final ThreadConfiguration DEFAULT_CONFIG =
- new ThreadConfiguration.Builder().build();
-
- 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 =
@@ -131,10 +122,12 @@
@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(false).build());
mController.leaveAndWait();
- mFtd = new FullThreadDevice(10 /* nodeId */);
}
@After
@@ -143,7 +136,6 @@
mController.setTestNetworkAsUpstreamAndWait(null);
mController.leaveAndWait();
- mController.setConfigurationAndWait(DEFAULT_CONFIG);
mFtd.destroy();
mExecutor.shutdownNow();
@@ -161,19 +153,6 @@
}
@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 otDaemonFactoryReset_deviceRoleIsStopped() throws Exception {
mController.joinAndWait(DEFAULT_DATASET);
@@ -228,6 +207,7 @@
}
@Test
+ @RequiresSimulationThreadDevice
public void udp_appStartEchoServer_endDeviceUdpEchoSuccess() throws Exception {
// Topology:
// Test App ------ thread-wpan ------ End Device
@@ -246,30 +226,25 @@
}
@Test
- public void joinNetworkWithBrDisabled_meshLocalAddressesArePreferred() throws Exception {
- // When BR feature is disabled, there is no OMR address, so the mesh-local addresses are
- // expected to be preferred.
- mOtCtl.executeCommand("br disable");
+ public void joinNetwork_onlyMlEidIsPreferred() throws Exception {
mController.joinAndWait(DEFAULT_DATASET);
+ waitFor(
+ () -> getIpv6Addresses("thread-wpan").contains(mOtCtl.getMlEid()),
+ LINK_ADDR_TIMEOUT);
IpPrefix meshLocalPrefix = DEFAULT_DATASET.getMeshLocalPrefix();
- List<LinkAddress> linkAddresses = getIpv6LinkAddresses("thread-wpan");
- for (LinkAddress address : linkAddresses) {
- if (meshLocalPrefix.contains(address.getAddress())) {
- assertThat(address.getDeprecationTime())
- .isGreaterThan(SystemClock.elapsedRealtime());
- assertThat(address.isPreferred()).isTrue();
- }
- }
-
- mOtCtl.executeCommand("br enable");
- }
-
- @Test
- public void joinNetwork_tunInterfaceJoinsAllRouterMulticastGroup() throws Exception {
- mController.joinAndWait(DEFAULT_DATASET);
-
- assertTunInterfaceMemberOfGroup(GROUP_ADDR_ALL_ROUTERS);
+ var linkAddrs = FluentIterable.from(getIpv6LinkAddresses("thread-wpan"));
+ var meshLocalAddrs = linkAddrs.filter(addr -> meshLocalPrefix.contains(addr.getAddress()));
+ var mlEidAddrs = meshLocalAddrs.filter(addr -> addr.getAddress().equals(mOtCtl.getMlEid()));
+ var nonMlEidAddrs = meshLocalAddrs.filter(addr -> !mlEidAddrs.contains(addr));
+ assertThat(mlEidAddrs).hasSize(1);
+ assertThat(mlEidAddrs.allMatch(addr -> addr.isPreferred())).isTrue();
+ assertThat(mlEidAddrs.allMatch(addr -> addr.getDeprecationTime() > elapsedRealtime()))
+ .isTrue();
+ assertThat(nonMlEidAddrs).isNotEmpty();
+ assertThat(nonMlEidAddrs.allMatch(addr -> !addr.isPreferred())).isTrue();
+ assertThat(nonMlEidAddrs.allMatch(addr -> addr.getDeprecationTime() <= elapsedRealtime()))
+ .isTrue();
}
@Test
@@ -287,6 +262,7 @@
}
@Test
+ @RequiresSimulationThreadDevice
public void edPingsMeshLocalAddresses_oneReplyPerRequest() throws Exception {
mController.joinAndWait(DEFAULT_DATASET);
startFtdChild(mFtd, DEFAULT_DATASET);
@@ -302,80 +278,15 @@
}
@Test
- public void addPrefixToNetData_routeIsAddedToTunInterface() throws Exception {
- ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
- 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);
-
- LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
- assertThat(lp).isNotNull();
- assertThat(lp.getRoutes().stream().anyMatch(r -> r.matches(TEST_NO_SLAAC_PREFIX_ADDRESS)))
- .isTrue();
- }
-
- @Test
- public void removePrefixFromNetData_routeIsRemovedFromTunInterface() throws Exception {
- ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
- 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);
-
- LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
- assertThat(lp).isNotNull();
- assertThat(lp.getRoutes().stream().anyMatch(r -> r.matches(TEST_NO_SLAAC_PREFIX_ADDRESS)))
- .isFalse();
- }
-
- @Test
- public void toggleThreadNetwork_routeFromPreviousNetDataIsRemoved() throws Exception {
- ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
- mController.joinAndWait(DEFAULT_DATASET);
- mOtCtl.executeCommand("prefix add " + TEST_NO_SLAAC_PREFIX + " pros med");
- mOtCtl.executeCommand("netdata register");
-
- mController.leaveAndWait();
- mController.joinAndWait(DEFAULT_DATASET);
-
- LinkProperties lp = cm.getLinkProperties(getThreadNetwork(CALLBACK_TIMEOUT));
- assertThat(lp).isNotNull();
- assertThat(lp.getRoutes().stream().anyMatch(r -> r.matches(TEST_NO_SLAAC_PREFIX_ADDRESS)))
- .isFalse();
- }
-
- @Test
- public void setConfiguration_disableBorderRouter_noBrfunctionsEnabled() throws Exception {
- NetworkRequest request =
- new NetworkRequest.Builder()
- .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
- .build();
+ @RequiresSimulationThreadDevice
+ public void setConfiguration_disableBorderRouter_borderRoutingDisabled() throws Exception {
startFtdLeader(mFtd, DEFAULT_DATASET);
mController.setConfigurationAndWait(
new ThreadConfiguration.Builder().setBorderRouterEnabled(false).build());
mController.joinAndWait(DEFAULT_DATASET);
- NetworkCapabilities caps = registerNetworkCallbackAndWait(request);
- assertThat(caps.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)).isFalse();
assertThat(mOtCtl.getBorderRoutingState()).ignoringCase().isEqualTo("disabled");
- assertThat(mOtCtl.getSrpServerState()).ignoringCase().isNotEqualTo("disabled");
// TODO: b/376217403 - enables / disables Border Agent at runtime
}
@@ -441,8 +352,4 @@
throw new IllegalStateException(e);
}
}
-
- private void assertTunInterfaceMemberOfGroup(Inet6Address address) throws Exception {
- waitFor(() -> isInMulticastGroup(TUN_IF_NAME, address), TUN_ADDR_UPDATE_TIMEOUT);
- }
}
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
index dcccbf1..804a332 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
@@ -34,6 +34,7 @@
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.OtDaemonController;
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;
@@ -167,6 +168,7 @@
}
@Test
+ @RequiresSimulationThreadDevice
public void handleOtCtlCommand_pingFtd_getValidResponse() throws Exception {
mController.joinAndWait(DEFAULT_DATASET);
startFtdChild(mFtd, DEFAULT_DATASET);
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index bc8da8b..95ebda5 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -35,6 +35,7 @@
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_TESTING;
import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
+import static com.android.server.thread.ThreadPersistentSettings.KEY_THREAD_ENABLED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -42,6 +43,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@@ -93,7 +95,6 @@
import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.connectivity.resources.R;
@@ -112,6 +113,7 @@
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
@@ -124,6 +126,8 @@
import java.time.DateTimeException;
import java.time.Instant;
import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -132,7 +136,7 @@
/** Unit tests for {@link ThreadNetworkControllerService}. */
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(Parameterized.class)
// This test doesn't really need to run on the UI thread, but @Before and @Test annotated methods
// need to run in the same thread because there are code in {@code ThreadNetworkControllerService}
// checking that all its methods are running in the thread of the handler it's using. This is due
@@ -200,6 +204,17 @@
@Rule(order = 1)
public final TemporaryFolder tempFolder = new TemporaryFolder();
+ private final boolean mIsBorderRouterEnabled;
+
+ @Parameterized.Parameters
+ public static Collection configArguments() {
+ return Arrays.asList(new Object[][] {{false}, {true}});
+ }
+
+ public ThreadNetworkControllerServiceTest(boolean isBorderRouterEnabled) {
+ mIsBorderRouterEnabled = isBorderRouterEnabled;
+ }
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -231,6 +246,8 @@
when(mConnectivityResources.get()).thenReturn(mResources);
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
+ when(mResources.getBoolean(eq(R.bool.config_thread_border_router_default_enabled)))
+ .thenReturn(mIsBorderRouterEnabled);
when(mResources.getBoolean(
eq(R.bool.config_thread_srp_server_wait_for_border_routing_enabled)))
.thenReturn(true);
@@ -564,7 +581,7 @@
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
- assertThat(mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED)).isTrue();
+ assertThat(mPersistentSettings.get(KEY_THREAD_ENABLED)).isTrue();
}
@Test
@@ -920,7 +937,9 @@
}
@Test
- public void initialize_upstreamNetworkRequestHasCertainTransportTypesAndCapabilities() {
+ public void initialize_borderRouterEnabled_upstreamNetworkRequestHasExpectedTransportAndCaps() {
+ assumeTrue(mIsBorderRouterEnabled);
+
mService.initialize();
mTestLooper.dispatchAll();
@@ -999,7 +1018,8 @@
}
@Test
- public void activateEphemeralKeyMode_succeed() throws Exception {
+ public void activateEphemeralKeyMode_borderRouterEnabled_succeed() throws Exception {
+ assumeTrue(mIsBorderRouterEnabled);
mService.initialize();
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
@@ -1010,7 +1030,8 @@
}
@Test
- public void deactivateEphemeralKeyMode_succeed() throws Exception {
+ public void deactivateEphemeralKeyMode_borderRouterEnabled_succeed() throws Exception {
+ assumeTrue(mIsBorderRouterEnabled);
mService.initialize();
final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
index ca9741d..139f4c8 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
@@ -19,7 +19,7 @@
import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
-import static com.android.server.thread.ThreadPersistentSettings.THREAD_COUNTRY_CODE;
+import static com.android.server.thread.ThreadPersistentSettings.KEY_COUNTRY_CODE;
import static com.google.common.truth.Truth.assertThat;
@@ -454,7 +454,7 @@
@Test
public void settingsCountryCode_settingsCountryCodeIsActive_settingsCountryCodeIsUsed() {
- when(mPersistentSettings.get(THREAD_COUNTRY_CODE)).thenReturn(TEST_COUNTRY_CODE_CN);
+ when(mPersistentSettings.get(KEY_COUNTRY_CODE)).thenReturn(TEST_COUNTRY_CODE_CN);
mThreadNetworkCountryCode.initialize();
assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
index ba489d9..15f3d0b 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -16,8 +16,8 @@
package com.android.server.thread;
-import static com.android.server.thread.ThreadPersistentSettings.THREAD_COUNTRY_CODE;
-import static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED;
+import static com.android.server.thread.ThreadPersistentSettings.KEY_COUNTRY_CODE;
+import static com.android.server.thread.ThreadPersistentSettings.KEY_THREAD_ENABLED;
import static com.google.common.truth.Truth.assertThat;
@@ -35,6 +35,7 @@
import com.android.connectivity.resources.R;
import com.android.server.connectivity.ConnectivityResources;
+import com.android.server.thread.ThreadPersistentSettings.Key;
import org.junit.After;
import org.junit.Before;
@@ -83,68 +84,81 @@
@Test
public void initialize_readsFromFile() throws Exception {
- byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
+ byte[] data = createXmlForParsing(KEY_THREAD_ENABLED, false);
setupAtomicFileForRead(data);
mThreadPersistentSettings.initialize();
- assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+ assertThat(mThreadPersistentSettings.get(KEY_THREAD_ENABLED)).isFalse();
}
@Test
public void initialize_ThreadDisabledInResources_returnsThreadDisabled() throws Exception {
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
- setupAtomicFileForRead(new byte[0]);
+ mThreadPersistentSettings =
+ new ThreadPersistentSettings(mAtomicFile, mConnectivityResources);
mThreadPersistentSettings.initialize();
- assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+ assertThat(mThreadPersistentSettings.get(KEY_THREAD_ENABLED)).isFalse();
+ }
+
+ @Test
+ public void initialize_ThreadEnabledInResources_returnsThreadEnabled() throws Exception {
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
+ mThreadPersistentSettings =
+ new ThreadPersistentSettings(mAtomicFile, mConnectivityResources);
+
+ mThreadPersistentSettings.initialize();
+
+ assertThat(mThreadPersistentSettings.get(KEY_THREAD_ENABLED)).isTrue();
}
@Test
public void initialize_ThreadDisabledInResourcesButEnabledInXml_returnsThreadEnabled()
throws Exception {
when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
- byte[] data = createXmlForParsing(THREAD_ENABLED.key, true);
- setupAtomicFileForRead(data);
+ setupAtomicFileForRead(createXmlForParsing(KEY_THREAD_ENABLED, true));
+ mThreadPersistentSettings =
+ new ThreadPersistentSettings(mAtomicFile, mConnectivityResources);
mThreadPersistentSettings.initialize();
- assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
+ assertThat(mThreadPersistentSettings.get(KEY_THREAD_ENABLED)).isTrue();
}
@Test
public void put_ThreadFeatureEnabledTrue_returnsTrue() throws Exception {
- mThreadPersistentSettings.put(THREAD_ENABLED.key, true);
+ mThreadPersistentSettings.put(KEY_THREAD_ENABLED, true);
- assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
+ assertThat(mThreadPersistentSettings.get(KEY_THREAD_ENABLED)).isTrue();
}
@Test
public void put_ThreadFeatureEnabledFalse_returnsFalse() throws Exception {
- mThreadPersistentSettings.put(THREAD_ENABLED.key, false);
+ mThreadPersistentSettings.put(KEY_THREAD_ENABLED, false);
- assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+ assertThat(mThreadPersistentSettings.get(KEY_THREAD_ENABLED)).isFalse();
mThreadPersistentSettings.initialize();
- assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
+ assertThat(mThreadPersistentSettings.get(KEY_THREAD_ENABLED)).isFalse();
}
@Test
public void put_ThreadCountryCodeString_returnsString() throws Exception {
- mThreadPersistentSettings.put(THREAD_COUNTRY_CODE.key, TEST_COUNTRY_CODE);
+ mThreadPersistentSettings.put(KEY_COUNTRY_CODE, TEST_COUNTRY_CODE);
- assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
+ assertThat(mThreadPersistentSettings.get(KEY_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
mThreadPersistentSettings.initialize();
- assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
+ assertThat(mThreadPersistentSettings.get(KEY_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
}
@Test
public void put_ThreadCountryCodeNull_returnsNull() throws Exception {
- mThreadPersistentSettings.put(THREAD_COUNTRY_CODE.key, null);
+ mThreadPersistentSettings.put(KEY_COUNTRY_CODE, null);
- assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isNull();
+ assertThat(mThreadPersistentSettings.get(KEY_COUNTRY_CODE)).isNull();
mThreadPersistentSettings.initialize();
- assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isNull();
+ assertThat(mThreadPersistentSettings.get(KEY_COUNTRY_CODE)).isNull();
}
@Test
@@ -202,10 +216,10 @@
return new AtomicFile(mTemporaryFolder.newFile());
}
- private byte[] createXmlForParsing(String key, Boolean value) throws Exception {
+ private byte[] createXmlForParsing(Key<Boolean> key, Boolean value) throws Exception {
PersistableBundle bundle = new PersistableBundle();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- bundle.putBoolean(key, value);
+ bundle.putBoolean(key.key, value);
bundle.writeToStream(outputStream);
return outputStream.toByteArray();
}
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 98%
rename from thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
rename to thread/tests/utils/src/android/net/thread/utils/FullThreadDevice.java
index 38961a3..ed63fd0 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/utils/src/android/net/thread/utils/FullThreadDevice.java
@@ -239,6 +239,12 @@
executeCommand("udp send %s %d %s", serverAddr.getHostAddress(), serverPort, message);
}
+ /** Sets `true` to enable SRP server on this device. */
+ public void setSrpServerEnabled(boolean enabled) {
+ String cmd = enabled ? "enable" : "disable";
+ executeCommand("srp server " + cmd);
+ }
+
/** Enables the SRP client and run in autostart mode. */
public void autoStartSrpClient() {
executeCommand("srp client autostart enable");
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 92%
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 f00c9cd..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
}
}
@@ -479,15 +478,37 @@
return addresses
}
- /** Return the first discovered service of `serviceType`. */
+ /** Returns the list of [InetAddress] of the given network. */
+ @JvmStatic
+ fun getIpv6Addresses(interfaceName: String): List<InetAddress> {
+ return getIpv6LinkAddresses(interfaceName).map { it.address }
+ }
+
+ /** Return the first discovered service of `serviceType`. */
@JvmStatic
@Throws(Exception::class)
fun discoverService(nsdManager: NsdManager, serviceType: String): NsdServiceInfo {
+ return discoverService(nsdManager, serviceType, null)
+ }
+
+ /**
+ * Returns the service that matches `serviceType` and `serviceName`.
+ *
+ * If `serviceName` is null, returns the first discovered service. `serviceName` is not case
+ * sensitive.
+ */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun discoverService(nsdManager: NsdManager, serviceType: String, serviceName: String?):
+ NsdServiceInfo {
val serviceInfoFuture = CompletableFuture<NsdServiceInfo>()
val listener: NsdManager.DiscoveryListener = object : DefaultDiscoveryListener() {
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
Log.d(TAG, "onServiceFound: $serviceInfo")
- serviceInfoFuture.complete(serviceInfo)
+ if (serviceName == null ||
+ serviceInfo.getServiceName().equals(serviceName, true /* ignore case */)) {
+ serviceInfoFuture.complete(serviceInfo)
+ }
}
}
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, listener)
@@ -583,6 +604,17 @@
}
/**
+ * Let the FTD join the specified Thread network and wait for it becomes a Child or Router.
+ */
+ @JvmStatic
+ @Throws(Exception::class)
+ fun joinNetworkAndWait(ftd: FullThreadDevice, dataset: ActiveOperationalDataset) {
+ ftd.factoryReset()
+ ftd.joinNetwork(dataset)
+ ftd.waitForStateAnyOf(listOf("router", "child"), JOIN_TIMEOUT)
+ }
+
+ /**
* Let the FTD join the specified Thread network and wait for border routing to be available.
*
* @return the OMR address
@@ -613,6 +645,21 @@
controller.joinAndWait(dataset);
}
+ /** Enables Border Router and joins the specified Thread network. */
+ @JvmStatic
+ fun enableBorderRouterAndJoinNetwork(dataset: ActiveOperationalDataset) {
+ val context: Context = requireNotNull(ApplicationProvider.getApplicationContext());
+ val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context));
+
+ // TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
+ controller.leaveAndWait();
+
+ controller.setEnabledAndWait(true);
+ val config = ThreadConfiguration.Builder().setBorderRouterEnabled(true).build();
+ controller.setConfigurationAndWait(config);
+ controller.joinAndWait(dataset);
+ }
+
/** Leaves the Thread network and disables Thread. */
@JvmStatic
fun leaveNetworkAndDisableThread() {
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 88%
rename from thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
rename to thread/tests/utils/src/android/net/thread/utils/OtDaemonController.java
index 272685f..d35b94e 100644
--- a/thread/tests/integration/src/android/net/thread/utils/OtDaemonController.java
+++ b/thread/tests/utils/src/android/net/thread/utils/OtDaemonController.java
@@ -24,9 +24,11 @@
import com.android.compatibility.common.util.SystemUtil;
import java.net.Inet6Address;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.stream.Collectors;
/**
* Wrapper of the "/system/bin/ot-ctl" which can be used to send CLI commands to ot-daemon to
@@ -72,6 +74,25 @@
.toList();
}
+ /** Returns the OMR address of this device or {@code null} if it doesn't exist. */
+ @Nullable
+ public Inet6Address getOmrAddress() {
+ List<Inet6Address> allAddresses = new ArrayList<>(getAddresses());
+ allAddresses.removeAll(getMeshLocalAddresses());
+
+ List<Inet6Address> omrAddresses =
+ allAddresses.stream()
+ .filter(addr -> !addr.isLinkLocalAddress())
+ .collect(Collectors.toList());
+ if (omrAddresses.isEmpty()) {
+ return null;
+ } else if (omrAddresses.size() > 1) {
+ throw new IllegalStateException();
+ }
+
+ return omrAddresses.getFirst();
+ }
+
/** Returns {@code true} if the Thread interface is up. */
public boolean isInterfaceUp() {
String output = executeCommand("ifconfig");
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 97%
rename from thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
rename to thread/tests/utils/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
index b6114f3..c4150cb 100644
--- a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
+++ b/thread/tests/utils/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
@@ -233,7 +233,10 @@
public void setNat64EnabledAndWait(boolean enabled) throws Exception {
final ThreadConfiguration config = getConfiguration();
final ThreadConfiguration newConfig =
- new ThreadConfiguration.Builder(config).setNat64Enabled(enabled).build();
+ new ThreadConfiguration.Builder(config)
+ .setBorderRouterEnabled(true)
+ .setNat64Enabled(enabled)
+ .build();
setConfigurationAndWait(newConfig);
}