Merge "Migrate to netd_aidl_interface-V14" into main
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index dd60be7..414e50a 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -81,7 +81,10 @@
"framework-tethering.impl",
],
manifest: "AndroidManifestBase.xml",
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
// build tethering static library, used to compile both variants of the tethering.
@@ -215,7 +218,10 @@
use_embedded_native_libs: true,
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
sdk {
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 79d9a23..c065cd6 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -838,7 +838,7 @@
private void addInterfaceToNetwork(final int netId, @NonNull final String ifaceName) {
try {
- if (null != mRoutingCoordinator.value) {
+ if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
// TODO : remove this call in favor of using the LocalNetworkConfiguration
// correctly, which will let ConnectivityService do it automatically.
mRoutingCoordinator.value.addInterfaceToNetwork(netId, ifaceName);
@@ -852,7 +852,7 @@
private void addInterfaceForward(@NonNull final String fromIface,
@NonNull final String toIface) throws ServiceSpecificException, RemoteException {
- if (null != mRoutingCoordinator.value) {
+ if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
mRoutingCoordinator.value.addInterfaceForward(fromIface, toIface);
} else {
mNetd.tetherAddForward(fromIface, toIface);
@@ -862,7 +862,7 @@
private void removeInterfaceForward(@NonNull final String fromIface,
@NonNull final String toIface) {
- if (null != mRoutingCoordinator.value) {
+ if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
try {
mRoutingCoordinator.value.removeInterfaceForward(fromIface, toIface);
} catch (ServiceSpecificException e) {
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java b/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
index e7dc757..9ef0f45 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
@@ -19,18 +19,21 @@
import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_AIDL;
import android.annotation.NonNull;
+import android.annotation.TargetApi;
import android.hardware.tetheroffload.ForwardedStats;
import android.hardware.tetheroffload.IOffload;
import android.hardware.tetheroffload.ITetheringOffloadCallback;
import android.hardware.tetheroffload.NatTimeoutUpdate;
import android.hardware.tetheroffload.NetworkProtocol;
import android.hardware.tetheroffload.OffloadCallbackEvent;
+import android.os.Build;
import android.os.Handler;
import android.os.NativeHandle;
import android.os.ParcelFileDescriptor;
import android.os.ServiceManager;
import android.system.OsConstants;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.tethering.OffloadHardwareInterface.OffloadHalCallback;
@@ -40,6 +43,7 @@
/**
* The implementation of IOffloadHal which based on Stable AIDL interface
*/
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public class OffloadHalAidlImpl implements IOffloadHal {
private static final String TAG = OffloadHalAidlImpl.class.getSimpleName();
private static final String HAL_INSTANCE_NAME = IOffload.DESCRIPTOR + "/default";
@@ -52,6 +56,7 @@
private TetheringOffloadCallback mTetheringOffloadCallback;
+ @VisibleForTesting
public OffloadHalAidlImpl(int version, @NonNull IOffload offload, @NonNull Handler handler,
@NonNull SharedLog log) {
mOffloadVersion = version;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java b/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
index e0a9878..71922f9 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
@@ -74,10 +74,7 @@
*/
public boolean initOffload(@NonNull NativeHandle handle1, @NonNull NativeHandle handle2,
@NonNull OffloadHalCallback callback) {
- final String logmsg = String.format("initOffload(%d, %d, %s)",
- handle1.getFileDescriptor().getInt$(), handle2.getFileDescriptor().getInt$(),
- (callback == null) ? "null"
- : "0x" + Integer.toHexString(System.identityHashCode(callback)));
+ final String logmsg = "initOffload()";
mOffloadHalCallback = callback;
mTetheringOffloadCallback = new TetheringOffloadCallback(
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index b285d85..05cf9e8 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -500,8 +500,8 @@
method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void registerOperationalDatasetCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback);
method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
- method public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
- method public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
+ method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
field public static final int DEVICE_ROLE_CHILD = 2; // 0x2
field public static final int DEVICE_ROLE_DETACHED = 1; // 0x1
field public static final int DEVICE_ROLE_LEADER = 4; // 0x4
diff --git a/framework/src/android/net/QosSession.java b/framework/src/android/net/QosSession.java
index 25f3965..d1edae9 100644
--- a/framework/src/android/net/QosSession.java
+++ b/framework/src/android/net/QosSession.java
@@ -22,6 +22,9 @@
import android.os.Parcel;
import android.os.Parcelable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Provides identifying information of a QoS session. Sent to an application through
* {@link QosCallback}.
@@ -107,6 +110,7 @@
TYPE_EPS_BEARER,
TYPE_NR_BEARER,
})
+ @Retention(RetentionPolicy.SOURCE)
@interface QosSessionType {}
private QosSession(final Parcel in) {
diff --git a/nearby/framework/java/android/nearby/BroadcastRequest.java b/nearby/framework/java/android/nearby/BroadcastRequest.java
index 90f4d0f..6d6357d 100644
--- a/nearby/framework/java/android/nearby/BroadcastRequest.java
+++ b/nearby/framework/java/android/nearby/BroadcastRequest.java
@@ -88,6 +88,7 @@
* @hide
*/
@IntDef({MEDIUM_BLE})
+ @Retention(RetentionPolicy.SOURCE)
public @interface Medium {}
/**
diff --git a/nearby/framework/java/android/nearby/NearbyDevice.java b/nearby/framework/java/android/nearby/NearbyDevice.java
index e8fcc28..e7db0c5 100644
--- a/nearby/framework/java/android/nearby/NearbyDevice.java
+++ b/nearby/framework/java/android/nearby/NearbyDevice.java
@@ -25,6 +25,8 @@
import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -149,6 +151,7 @@
* @hide
*/
@IntDef({Medium.BLE, Medium.BLUETOOTH})
+ @Retention(RetentionPolicy.SOURCE)
public @interface Medium {
int BLE = 1;
int BLUETOOTH = 2;
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index a70b303..070a2b6 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -34,6 +34,8 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.WeakHashMap;
@@ -63,6 +65,7 @@
ScanStatus.SUCCESS,
ScanStatus.ERROR,
})
+ @Retention(RetentionPolicy.SOURCE)
public @interface ScanStatus {
// The undetermined status, some modules may be initializing. Retry is suggested.
int UNKNOWN = 0;
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 43357e4..ee5f25b 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -1645,8 +1645,8 @@
mContext, MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD))
.setIncludeInetAddressRecordsInProbing(mDeps.isFeatureEnabled(
mContext, MdnsFeatureFlags.INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING))
- .setIsExpiredServicesRemovalEnabled(mDeps.isTrunkStableFeatureEnabled(
- MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
+ .setIsExpiredServicesRemovalEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
.setIsLabelCountLimitEnabled(mDeps.isTetheringFeatureNotChickenedOut(
mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
.build();
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 738c151..0a6d8c1 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -86,7 +86,7 @@
public Builder() {
mIsMdnsOffloadFeatureEnabled = false;
mIncludeInetAddressRecordsInProbing = false;
- mIsExpiredServicesRemovalEnabled = true; // Default enabled.
+ mIsExpiredServicesRemovalEnabled = false;
mIsLabelCountLimitEnabled = true; // Default enabled.
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
index e2288c1..05ad1be 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -33,6 +33,7 @@
/** An mDNS response. */
public class MdnsResponse {
+ public static final long EXPIRATION_NEVER = Long.MAX_VALUE;
private final List<MdnsRecord> records;
private final List<MdnsPointerRecord> pointerRecords;
private MdnsServiceRecord serviceRecord;
@@ -349,6 +350,21 @@
return serviceName;
}
+ /** Get the min remaining ttl time from received records */
+ public long getMinRemainingTtl(long now) {
+ long minRemainingTtl = EXPIRATION_NEVER;
+ // TODO: Check other records(A, AAAA, TXT) ttl time.
+ if (!hasServiceRecord()) {
+ return EXPIRATION_NEVER;
+ }
+ // Check ttl time.
+ long remainingTtl = serviceRecord.getRemainingTTL(now);
+ if (remainingTtl < minRemainingTtl) {
+ minRemainingTtl = remainingTtl;
+ }
+ return minRemainingTtl;
+ }
+
/**
* Tests if this response is a goodbye message. This will be true if a service record is present
* and any of the records have a TTL of 0.
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index d3493c7..e9a41d1 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -16,16 +16,22 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsResponse.EXPIRATION_NEVER;
import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
import static com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase;
import static com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase;
+import static java.lang.Math.min;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Handler;
import android.os.Looper;
import android.util.ArrayMap;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -67,8 +73,11 @@
}
}
/**
- * A map of cached services. Key is composed of service name, type and socket. Value is the
- * service which use the service type to discover from each socket.
+ * A map of cached services. Key is composed of service type and socket. Value is the list of
+ * services which are discovered from the given CacheKey.
+ * When the MdnsFeatureFlags#NSD_EXPIRED_SERVICES_REMOVAL flag is enabled, the lists are sorted
+ * by expiration time, with the earliest entries appearing first. This sorting allows the
+ * removal process to progress through the expiration check efficiently.
*/
@NonNull
private final ArrayMap<CacheKey, List<MdnsResponse>> mCachedServices = new ArrayMap<>();
@@ -82,10 +91,20 @@
private final Handler mHandler;
@NonNull
private final MdnsFeatureFlags mMdnsFeatureFlags;
+ @NonNull
+ private final MdnsUtils.Clock mClock;
+ private long mNextExpirationTime = EXPIRATION_NEVER;
public MdnsServiceCache(@NonNull Looper looper, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+ this(looper, mdnsFeatureFlags, new MdnsUtils.Clock());
+ }
+
+ @VisibleForTesting
+ MdnsServiceCache(@NonNull Looper looper, @NonNull MdnsFeatureFlags mdnsFeatureFlags,
+ @NonNull MdnsUtils.Clock clock) {
mHandler = new Handler(looper);
mMdnsFeatureFlags = mdnsFeatureFlags;
+ mClock = clock;
}
/**
@@ -97,6 +116,9 @@
@NonNull
public List<MdnsResponse> getCachedServices(@NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
+ if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+ maybeRemoveExpiredServices(cacheKey, mClock.elapsedRealtime());
+ }
return mCachedServices.containsKey(cacheKey)
? Collections.unmodifiableList(new ArrayList<>(mCachedServices.get(cacheKey)))
: Collections.emptyList();
@@ -129,6 +151,9 @@
@Nullable
public MdnsResponse getCachedService(@NonNull String serviceName, @NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
+ if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+ maybeRemoveExpiredServices(cacheKey, mClock.elapsedRealtime());
+ }
final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
if (responses == null) {
return null;
@@ -137,6 +162,16 @@
return response != null ? new MdnsResponse(response) : null;
}
+ static void insertResponseAndSortList(
+ List<MdnsResponse> responses, MdnsResponse response, long now) {
+ // binarySearch returns "the index of the search key, if it is contained in the list;
+ // otherwise, (-(insertion point) - 1)"
+ final int searchRes = Collections.binarySearch(responses, response,
+ // Sort the list by ttl.
+ (o1, o2) -> Long.compare(o1.getMinRemainingTtl(now), o2.getMinRemainingTtl(now)));
+ responses.add(searchRes >= 0 ? searchRes : (-searchRes - 1), response);
+ }
+
/**
* Add or update a service.
*
@@ -151,7 +186,15 @@
final MdnsResponse existing =
findMatchedResponse(responses, response.getServiceInstanceName());
responses.remove(existing);
- responses.add(response);
+ if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+ final long now = mClock.elapsedRealtime();
+ // Insert and sort service
+ insertResponseAndSortList(responses, response, now);
+ // Update the next expiration check time when a new service is added.
+ mNextExpirationTime = getNextExpirationTime(now);
+ } else {
+ responses.add(response);
+ }
}
/**
@@ -168,14 +211,25 @@
return null;
}
final Iterator<MdnsResponse> iterator = responses.iterator();
+ MdnsResponse removedResponse = null;
while (iterator.hasNext()) {
final MdnsResponse response = iterator.next();
if (equalsIgnoreDnsCase(serviceName, response.getServiceInstanceName())) {
iterator.remove();
- return response;
+ removedResponse = response;
+ break;
}
}
- return null;
+
+ if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+ // Remove the serviceType if no response.
+ if (responses.isEmpty()) {
+ mCachedServices.remove(cacheKey);
+ }
+ // Update the next expiration check time when a service is removed.
+ mNextExpirationTime = getNextExpirationTime(mClock.elapsedRealtime());
+ }
+ return removedResponse;
}
/**
@@ -203,6 +257,87 @@
mCallbacks.remove(cacheKey);
}
+ private void notifyServiceExpired(@NonNull CacheKey cacheKey,
+ @NonNull MdnsResponse previousResponse, @Nullable MdnsResponse newResponse) {
+ final ServiceExpiredCallback callback = mCallbacks.get(cacheKey);
+ if (callback == null) {
+ // The cached service is no listener.
+ return;
+ }
+ mHandler.post(()-> callback.onServiceRecordExpired(previousResponse, newResponse));
+ }
+
+ static List<MdnsResponse> removeExpiredServices(@NonNull List<MdnsResponse> responses,
+ long now) {
+ final List<MdnsResponse> removedResponses = new ArrayList<>();
+ final Iterator<MdnsResponse> iterator = responses.iterator();
+ while (iterator.hasNext()) {
+ final MdnsResponse response = iterator.next();
+ // TODO: Check other records (A, AAAA, TXT) ttl time and remove the record if it's
+ // expired. Then send service update notification.
+ if (!response.hasServiceRecord() || response.getMinRemainingTtl(now) > 0) {
+ // The responses are sorted by the service record ttl time. Break out of loop
+ // early if service is not expired or no service record.
+ break;
+ }
+ // Remove the ttl expired service.
+ iterator.remove();
+ removedResponses.add(response);
+ }
+ return removedResponses;
+ }
+
+ private long getNextExpirationTime(long now) {
+ if (mCachedServices.isEmpty()) {
+ return EXPIRATION_NEVER;
+ }
+
+ long minRemainingTtl = EXPIRATION_NEVER;
+ for (int i = 0; i < mCachedServices.size(); i++) {
+ minRemainingTtl = min(minRemainingTtl,
+ // The empty lists are not kept in the map, so there's always at least one
+ // element in the list. Therefore, it's fine to get the first element without a
+ // null check.
+ mCachedServices.valueAt(i).get(0).getMinRemainingTtl(now));
+ }
+ return minRemainingTtl == EXPIRATION_NEVER ? EXPIRATION_NEVER : now + minRemainingTtl;
+ }
+
+ /**
+ * Check whether the ttl time is expired on each service and notify to the listeners
+ */
+ private void maybeRemoveExpiredServices(CacheKey cacheKey, long now) {
+ ensureRunningOnHandlerThread(mHandler);
+ if (now < mNextExpirationTime) {
+ // Skip the check if ttl time is not expired.
+ return;
+ }
+
+ final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
+ if (responses == null) {
+ // No such services.
+ return;
+ }
+
+ final List<MdnsResponse> removedResponses = removeExpiredServices(responses, now);
+ if (removedResponses.isEmpty()) {
+ // No expired services.
+ return;
+ }
+
+ for (MdnsResponse previousResponse : removedResponses) {
+ notifyServiceExpired(cacheKey, previousResponse, null /* newResponse */);
+ }
+
+ // Remove the serviceType if no response.
+ if (responses.isEmpty()) {
+ mCachedServices.remove(cacheKey);
+ }
+
+ // Update next expiration time.
+ mNextExpirationTime = getNextExpirationTime(now);
+ }
+
/*** Callbacks for listening service expiration */
public interface ServiceExpiredCallback {
/*** Notify the service is expired */
@@ -210,5 +345,5 @@
@Nullable MdnsResponse newResponse);
}
- // TODO: check ttl expiration for each service and notify to the clients.
+ // TODO: Schedule a job to check ttl expiration for all services and notify to the clients.
}
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 0a03186..32f604e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -312,8 +312,7 @@
this.searchOptions = searchOptions;
boolean hadReply = false;
if (listeners.put(listener, searchOptions) == null) {
- for (MdnsResponse existingResponse :
- serviceCache.getCachedServices(cacheKey)) {
+ for (MdnsResponse existingResponse : serviceCache.getCachedServices(cacheKey)) {
if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
final MdnsServiceInfo info =
buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 7d1644e..f904cd1 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -969,6 +969,9 @@
// Flag to optimize closing frozen app sockets by waiting for the cellular modem to wake up.
private final boolean mDelayDestroyFrozenSockets;
+ // Flag to allow SysUI to receive connectivity reports for wifi picker UI.
+ private final boolean mAllowSysUiConnectivityReports;
+
// Uids that ConnectivityService is pending to close sockets of.
private final Set<Integer> mPendingFrozenUids = new ArraySet<>();
@@ -1469,6 +1472,13 @@
}
/**
+ * @see DeviceConfigUtils#isTetheringFeatureNotChickenedOut
+ */
+ public boolean isFeatureNotChickenedOut(Context context, String name) {
+ return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, name);
+ }
+
+ /**
* Get the BpfNetMaps implementation to use in ConnectivityService.
* @param netd a netd binder
* @return BpfNetMaps implementation.
@@ -1760,7 +1770,12 @@
mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter,
null /* broadcastPermission */, mHandler);
- mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mNetd, mHandler);
+ // TrackMultiNetworkActivities feature should be enabled by trunk stable flag.
+ // But reading the trunk stable flags from mainline modules is not supported yet.
+ // So enabling this feature on V+ release.
+ mTrackMultiNetworkActivities = mDeps.isAtLeastV();
+ mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mNetd, mHandler,
+ mTrackMultiNetworkActivities);
final NetdCallback netdCallback = new NetdCallback();
try {
@@ -1835,6 +1850,8 @@
&& mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION);
mDelayDestroyFrozenSockets = mDeps.isAtLeastU()
&& mDeps.isFeatureEnabled(context, DELAY_DESTROY_FROZEN_SOCKETS_VERSION);
+ mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
+ mContext, ALLOW_SYSUI_CONNECTIVITY_REPORTS);
if (mDestroyFrozenSockets) {
final UidFrozenStateChangedCallback frozenStateChangedCallback =
new UidFrozenStateChangedCallback() {
@@ -3234,9 +3251,20 @@
private void handleReportNetworkActivity(final NetworkActivityParams params) {
mNetworkActivityTracker.handleReportNetworkActivity(params);
+ final boolean isCellNetworkActivity;
+ if (mTrackMultiNetworkActivities) {
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(params.label);
+ // nai could be null if netd receives a netlink message and calls the network
+ // activity change callback after the network is unregistered from ConnectivityService.
+ isCellNetworkActivity = nai != null
+ && nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
+ } else {
+ isCellNetworkActivity = params.label == TRANSPORT_CELLULAR;
+ }
+
if (mDelayDestroyFrozenSockets
&& params.isActive
- && params.label == TRANSPORT_CELLULAR
+ && isCellNetworkActivity
&& !mPendingFrozenUids.isEmpty()) {
closePendingFrozenSockets();
}
@@ -3297,6 +3325,10 @@
static final String DELAY_DESTROY_FROZEN_SOCKETS_VERSION =
"delay_destroy_frozen_sockets_version";
+ @VisibleForTesting
+ public static final String ALLOW_SYSUI_CONNECTIVITY_REPORTS =
+ "allow_sysui_connectivity_reports";
+
private void enforceInternetPermission() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.INTERNET,
@@ -3460,6 +3492,11 @@
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
+ private boolean checkSystemBarServicePermission(int pid, int uid) {
+ return checkAnyPermissionOf(mContext, pid, uid,
+ android.Manifest.permission.STATUS_BAR_SERVICE);
+ }
+
private boolean checkNetworkSignalStrengthWakeupPermission(int pid, int uid) {
return checkAnyPermissionOf(mContext, pid, uid,
android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP,
@@ -4944,6 +4981,11 @@
if (wasDefault) {
mDefaultInetConditionPublished = 0;
}
+ if (mTrackMultiNetworkActivities) {
+ // If trackMultiNetworkActivities is disabled, ActivityTracker removes idleTimer when
+ // the network becomes no longer the default network.
+ mNetworkActivityTracker.removeDataActivityTracking(nai);
+ }
notifyIfacesChangedForNetworkStats();
// If this was a local network forwarded to some upstream, or if some local network was
// forwarded to this nai, then disable forwarding rules now.
@@ -4997,12 +5039,7 @@
}
if (mDefaultRequest == nri) {
- // TODO : make battery stats aware that since 2013 multiple interfaces may be
- // active at the same time. For now keep calling this with the default
- // network, because while incorrect this is the closest to the old (also
- // incorrect) behavior.
- mNetworkActivityTracker.updateDataActivityTracking(
- null /* newNetwork */, nai);
+ mNetworkActivityTracker.updateDefaultNetwork(null /* newNetwork */, nai);
maybeClosePendingFrozenSockets(null /* newNetwork */, nai);
ensureNetworkTransitionWakelock(nai.toShortString());
}
@@ -9623,7 +9660,7 @@
if (oldDefaultNetwork != null) {
mLingerMonitor.noteLingerDefaultNetwork(oldDefaultNetwork, newDefaultNetwork);
}
- mNetworkActivityTracker.updateDataActivityTracking(newDefaultNetwork, oldDefaultNetwork);
+ mNetworkActivityTracker.updateDefaultNetwork(newDefaultNetwork, oldDefaultNetwork);
maybeClosePendingFrozenSockets(newDefaultNetwork, oldDefaultNetwork);
mProxyTracker.setDefaultProxy(null != newDefaultNetwork
? newDefaultNetwork.linkProperties.getHttpProxy() : null);
@@ -10523,6 +10560,15 @@
SystemClock.elapsedRealtime(), mNascentDelayMs);
networkAgent.setInactive();
+ if (mTrackMultiNetworkActivities) {
+ // Start tracking activity of this network.
+ // This must be called before rematchAllNetworksAndRequests since the network
+ // should be tracked when the network becomes the default network.
+ // This method does not trigger any callbacks or broadcasts. Callbacks or broadcasts
+ // can be triggered later if this network becomes the default network.
+ mNetworkActivityTracker.setupDataActivityTracking(networkAgent);
+ }
+
// Consider network even though it is not yet validated.
rematchAllNetworksAndRequests();
@@ -11581,6 +11627,10 @@
if (checkNetworkStackPermission(callbackPid, callbackUid)) {
return true;
}
+ if (mAllowSysUiConnectivityReports
+ && checkSystemBarServicePermission(callbackPid, callbackUid)) {
+ return true;
+ }
// Administrator UIDs also contains the Owner UID
final int[] administratorUids = nai.networkCapabilities.getAdministratorUids();
@@ -11710,8 +11760,8 @@
*/
private static final class NetworkActivityParams {
public final boolean isActive;
- // Label used for idle timer. Transport type is used as label.
- // label is int since NMS was using the identifier as int, and it has not been changed
+ // If TrackMultiNetworkActivities is enabled, idleTimer label is netid.
+ // If TrackMultiNetworkActivities is disabled, idleTimer label is transport type.
public final int label;
public final long timestampNs;
// Uid represents the uid that was responsible for waking the radio.
@@ -11753,13 +11803,15 @@
}
}
+ private final boolean mTrackMultiNetworkActivities;
private final LegacyNetworkActivityTracker mNetworkActivityTracker;
/**
* Class used for updating network activity tracking with netd and notify network activity
* changes.
*/
- private static final class LegacyNetworkActivityTracker {
+ @VisibleForTesting
+ public static final class LegacyNetworkActivityTracker {
private static final int NO_UID = -1;
private final Context mContext;
private final INetd mNetd;
@@ -11771,8 +11823,14 @@
// If there is no default network, default network is considered active to keep the existing
// behavior. Initial value is used until first connect to the default network.
private volatile boolean mIsDefaultNetworkActive = true;
+ private Network mDefaultNetwork;
// Key is netId. Value is configured idle timer information.
private final SparseArray<IdleTimerParams> mActiveIdleTimers = new SparseArray<>();
+ private final boolean mTrackMultiNetworkActivities;
+ // Store netIds of Wi-Fi networks whose idletimers report that they are active
+ private final Set<Integer> mActiveWifiNetworks = new ArraySet<>();
+ // Store netIds of cellular networks whose idletimers report that they are active
+ private final Set<Integer> mActiveCellularNetworks = new ArraySet<>();
private static class IdleTimerParams {
public final int timeout;
@@ -11785,10 +11843,11 @@
}
LegacyNetworkActivityTracker(@NonNull Context context, @NonNull INetd netd,
- @NonNull Handler handler) {
+ @NonNull Handler handler, boolean trackMultiNetworkActivities) {
mContext = context;
mNetd = netd;
mHandler = handler;
+ mTrackMultiNetworkActivities = trackMultiNetworkActivities;
}
private void ensureRunningOnConnectivityServiceThread() {
@@ -11798,19 +11857,97 @@
}
}
- public void handleReportNetworkActivity(NetworkActivityParams activityParams) {
- ensureRunningOnConnectivityServiceThread();
+ /**
+ * Update network activity and call BatteryStats to update radio power state if the
+ * mobile or Wi-Fi activity is changed.
+ * LegacyNetworkActivityTracker considers the mobile network is active if at least one
+ * mobile network is active since BatteryStatsService only maintains a single power state
+ * for the mobile network.
+ * The Wi-Fi network is also the same.
+ *
+ * {@link #setupDataActivityTracking} and {@link #removeDataActivityTracking} use
+ * TRANSPORT_CELLULAR as the transportType argument if the network has both cell and Wi-Fi
+ * transports.
+ */
+ private void maybeUpdateRadioPowerState(final int netId, final int transportType,
+ final boolean isActive, final int uid) {
+ if (transportType != TRANSPORT_WIFI && transportType != TRANSPORT_CELLULAR) {
+ Log.e(TAG, "Unexpected transportType in maybeUpdateRadioPowerState: "
+ + transportType);
+ return;
+ }
+ final Set<Integer> activeNetworks = transportType == TRANSPORT_WIFI
+ ? mActiveWifiNetworks : mActiveCellularNetworks;
+
+ final boolean wasEmpty = activeNetworks.isEmpty();
+ if (isActive) {
+ activeNetworks.add(netId);
+ } else {
+ activeNetworks.remove(netId);
+ }
+
+ if (wasEmpty != activeNetworks.isEmpty()) {
+ updateRadioPowerState(isActive, transportType, uid);
+ }
+ }
+
+ private void handleDefaultNetworkActivity(final int transportType,
+ final boolean isActive, final long timestampNs) {
+ mIsDefaultNetworkActive = isActive;
+ sendDataActivityBroadcast(transportTypeToLegacyType(transportType),
+ isActive, timestampNs);
+ if (isActive) {
+ reportNetworkActive();
+ }
+ }
+
+ private void handleReportNetworkActivityWithNetIdLabel(
+ NetworkActivityParams activityParams) {
+ final int netId = activityParams.label;
+ final IdleTimerParams idleTimerParams = mActiveIdleTimers.get(netId);
+ if (idleTimerParams == null) {
+ // This network activity change is not tracked anymore
+ // This can happen if netd callback post activity change event message but idle
+ // timer is removed before processing this message.
+ return;
+ }
+ // TODO: if a network changes transports, storing the transport type in the
+ // IdleTimerParams is not correct. Consider getting it from the network's
+ // NetworkCapabilities instead.
+ final int transportType = idleTimerParams.transportType;
+ maybeUpdateRadioPowerState(netId, transportType,
+ activityParams.isActive, activityParams.uid);
+
+ if (mDefaultNetwork == null || mDefaultNetwork.netId != netId) {
+ // This activity change is not for the default network.
+ return;
+ }
+
+ handleDefaultNetworkActivity(transportType, activityParams.isActive,
+ activityParams.timestampNs);
+ }
+
+ private void handleReportNetworkActivityWithTransportTypeLabel(
+ NetworkActivityParams activityParams) {
if (mActiveIdleTimers.size() == 0) {
// This activity change is not for the current default network.
// This can happen if netd callback post activity change event message but
// the default network is lost before processing this message.
return;
}
- sendDataActivityBroadcast(transportTypeToLegacyType(activityParams.label),
- activityParams.isActive, activityParams.timestampNs);
- mIsDefaultNetworkActive = activityParams.isActive;
- if (mIsDefaultNetworkActive) {
- reportNetworkActive();
+ handleDefaultNetworkActivity(activityParams.label, activityParams.isActive,
+ activityParams.timestampNs);
+ }
+
+ /**
+ * Handle network activity change
+ */
+ public void handleReportNetworkActivity(NetworkActivityParams activityParams) {
+ ensureRunningOnConnectivityServiceThread();
+ if (mTrackMultiNetworkActivities) {
+ handleReportNetworkActivityWithNetIdLabel(activityParams);
+ } else {
+ handleReportNetworkActivityWithTransportTypeLabel(activityParams);
}
}
@@ -11867,6 +12004,30 @@
}
/**
+ * Get idle timer label
+ */
+ @VisibleForTesting
+ public static int getIdleTimerLabel(final boolean trackMultiNetworkActivities,
+ final int netId, final int transportType) {
+ return trackMultiNetworkActivities ? netId : transportType;
+ }
+
+ private boolean maybeCreateIdleTimer(
+ String iface, int netId, int timeout, int transportType) {
+ if (timeout <= 0 || iface == null) return false;
+ try {
+ final String label = Integer.toString(getIdleTimerLabel(
+ mTrackMultiNetworkActivities, netId, transportType));
+ mNetd.idletimerAddInterface(iface, timeout, label);
+ mActiveIdleTimers.put(netId, new IdleTimerParams(timeout, transportType));
+ return true;
+ } catch (Exception e) {
+ loge("Exception in createIdleTimer", e);
+ return false;
+ }
+ }
+
+ /**
* Setup data activity tracking for the given network.
*
* Every {@code setupDataActivityTracking} should be paired with a
@@ -11875,13 +12036,17 @@
* @return true if the idleTimer is added to the network, false otherwise
*/
private boolean setupDataActivityTracking(NetworkAgentInfo networkAgent) {
+ ensureRunningOnConnectivityServiceThread();
final String iface = networkAgent.linkProperties.getInterfaceName();
final int netId = networkAgent.network().netId;
final int timeout;
final int type;
- if (networkAgent.networkCapabilities.hasTransport(
+ if (!networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_VPN)) {
+ // Do not track VPN network.
+ return false;
+ } else if (networkAgent.networkCapabilities.hasTransport(
NetworkCapabilities.TRANSPORT_CELLULAR)) {
timeout = Settings.Global.getInt(mContext.getContentResolver(),
ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_MOBILE,
@@ -11897,25 +12062,21 @@
return false; // do not track any other networks
}
- updateRadioPowerState(true /* isActive */, type);
-
- if (timeout > 0 && iface != null) {
- try {
- mActiveIdleTimers.put(netId, new IdleTimerParams(timeout, type));
- mNetd.idletimerAddInterface(iface, timeout, Integer.toString(type));
- return true;
- } catch (Exception e) {
- // You shall not crash!
- loge("Exception in setupDataActivityTracking " + e);
- }
+ final boolean hasIdleTimer = maybeCreateIdleTimer(iface, netId, timeout, type);
+ if (hasIdleTimer || !mTrackMultiNetworkActivities) {
+ // If trackMultiNetwork is disabled, NetworkActivityTracker updates radio power
+ // state in all cases. If trackMultiNetwork is enabled, it updates radio power
+ // state only about a network that has an idletimer.
+ maybeUpdateRadioPowerState(netId, type, true /* isActive */, NO_UID);
}
- return false;
+ return hasIdleTimer;
}
/**
* Remove data activity tracking when network disconnects.
*/
- private void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+ public void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+ ensureRunningOnConnectivityServiceThread();
final String iface = networkAgent.linkProperties.getInterfaceName();
final int netId = networkAgent.network().netId;
final NetworkCapabilities caps = networkAgent.networkCapabilities;
@@ -11923,7 +12084,10 @@
if (iface == null) return;
final int type;
- if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+ if (!networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_VPN)) {
+ // Do not track VPN network.
+ return;
+ } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
type = NetworkCapabilities.TRANSPORT_CELLULAR;
} else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
type = NetworkCapabilities.TRANSPORT_WIFI;
@@ -11932,16 +12096,17 @@
}
try {
- updateRadioPowerState(false /* isActive */, type);
+ maybeUpdateRadioPowerState(netId, type, false /* isActive */, NO_UID);
final IdleTimerParams params = mActiveIdleTimers.get(netId);
if (params == null) {
// IdleTimer is not added if the configured timeout is 0 or negative value
return;
}
mActiveIdleTimers.remove(netId);
- // The call fails silently if no idle timer setup for this interface
- mNetd.idletimerRemoveInterface(iface, params.timeout,
- Integer.toString(params.transportType));
+ final String label = Integer.toString(getIdleTimerLabel(
+ mTrackMultiNetworkActivities, netId, params.transportType));
+ // The call fails silently if no idle timer setup for this interface
+ mNetd.idletimerRemoveInterface(iface, params.timeout, label);
} catch (Exception e) {
// You shall not crash!
loge("Exception in removeDataActivityTracking " + e);
@@ -11951,12 +12116,15 @@
private void updateDefaultNetworkActivity(NetworkAgentInfo defaultNetwork,
boolean hasIdleTimer) {
if (defaultNetwork != null) {
+ mDefaultNetwork = defaultNetwork.network();
mIsDefaultNetworkActive = true;
- // Callbacks are called only when the network has the idle timer.
- if (hasIdleTimer) {
+ // If only the default network is tracked, callbacks are called only when the
+ // network has the idle timer.
+ if (mTrackMultiNetworkActivities || hasIdleTimer) {
reportNetworkActive();
}
} else {
+ mDefaultNetwork = null;
// If there is no default network, default network is considered active to keep the
// existing behavior.
mIsDefaultNetworkActive = true;
@@ -11964,29 +12132,34 @@
}
/**
- * Update data activity tracking when network state is updated.
+ * Update the default network this class tracks the activity of.
*/
- public void updateDataActivityTracking(NetworkAgentInfo newNetwork,
+ public void updateDefaultNetwork(NetworkAgentInfo newNetwork,
NetworkAgentInfo oldNetwork) {
ensureRunningOnConnectivityServiceThread();
+ // If TrackMultiNetworkActivities is enabled, devices add idleTimer when the network is
+ // first connected and remove when the network is disconnected.
+ // If TrackMultiNetworkActivities is disabled, devices add idleTimer when the network
+ // becomes the default network and remove when the network becomes no longer the default
+ // network.
boolean hasIdleTimer = false;
- if (newNetwork != null) {
+ if (!mTrackMultiNetworkActivities && newNetwork != null) {
hasIdleTimer = setupDataActivityTracking(newNetwork);
}
updateDefaultNetworkActivity(newNetwork, hasIdleTimer);
- if (oldNetwork != null) {
+ if (!mTrackMultiNetworkActivities && oldNetwork != null) {
removeDataActivityTracking(oldNetwork);
}
}
- private void updateRadioPowerState(boolean isActive, int transportType) {
+ private void updateRadioPowerState(boolean isActive, int transportType, int uid) {
final BatteryStatsManager bs = mContext.getSystemService(BatteryStatsManager.class);
switch (transportType) {
case NetworkCapabilities.TRANSPORT_CELLULAR:
- bs.reportMobileRadioPowerState(isActive, NO_UID);
+ bs.reportMobileRadioPowerState(isActive, uid);
break;
case NetworkCapabilities.TRANSPORT_WIFI:
- bs.reportWifiRadioPowerState(isActive, NO_UID);
+ bs.reportWifiRadioPowerState(isActive, uid);
break;
default:
logw("Untracked transport type:" + transportType);
@@ -12006,7 +12179,9 @@
}
public void dump(IndentingPrintWriter pw) {
+ pw.print("mTrackMultiNetworkActivities="); pw.println(mTrackMultiNetworkActivities);
pw.print("mIsDefaultNetworkActive="); pw.println(mIsDefaultNetworkActive);
+ pw.print("mDefaultNetwork="); pw.println(mDefaultNetwork);
pw.println("Idle timers:");
try {
for (int i = 0; i < mActiveIdleTimers.size(); i++) {
@@ -12015,11 +12190,13 @@
pw.print(" timeout="); pw.print(params.timeout);
pw.print(" type="); pw.println(params.transportType);
}
+ pw.println("WiFi active networks: " + mActiveWifiNetworks);
+ pw.println("Cellular active networks: " + mActiveCellularNetworks);
} catch (Exception e) {
- // mActiveIdleTimers should only be accessed from handler thread, except dump().
- // As dump() is never called in normal usage, it would be needlessly expensive
- // to lock the collection only for its benefit.
- // Also, mActiveIdleTimers is not expected to be updated frequently.
+ // mActiveIdleTimers, mActiveWifiNetworks, and mActiveCellularNetworks should only
+ // be accessed from handler thread, except dump(). As dump() is never called in
+ // normal usage, it would be needlessly expensive to lock the collection only for
+ // its benefit. Also, they are not expected to be updated frequently.
// So catching the exception and logging.
pw.println("Failed to dump NetworkActivityTracker: " + e);
}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java b/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
index c7ed3e6..d298599 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
@@ -21,7 +21,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.text.TextUtils;
import android.util.Log;
import java.net.InetAddress;
@@ -29,7 +28,6 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import java.util.StringJoiner;
/**
* A class for a DNS SVCB response packet.
@@ -159,16 +157,6 @@
return out;
}
- @Override
- public String toString() {
- final StringJoiner out = new StringJoiner(" ");
- out.add("QUERY: [" + TextUtils.join(", ", mRecords[QDSECTION]) + "]");
- out.add("ANSWER: [" + TextUtils.join(", ", mRecords[ANSECTION]) + "]");
- out.add("AUTHORITY: [" + TextUtils.join(", ", mRecords[NSSECTION]) + "]");
- out.add("ADDITIONAL: [" + TextUtils.join(", ", mRecords[ARSECTION]) + "]");
- return out.toString();
- }
-
/**
* Creates a DnsSvcbPacket object from the given wire-format DNS answer.
*/
diff --git a/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java b/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
index 669725c..935cdf6 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
@@ -230,7 +230,7 @@
/**
* The base class for all SvcParam.
*/
- private abstract static class SvcParam {
+ private abstract static class SvcParam<T> {
private final int mKey;
SvcParam(int key) {
@@ -240,9 +240,11 @@
int getKey() {
return mKey;
}
+
+ abstract T getValue();
}
- private static class SvcParamMandatory extends SvcParam {
+ private static class SvcParamMandatory extends SvcParam<short[]> {
private final short[] mValue;
private SvcParamMandatory(@NonNull ByteBuffer buf) throws BufferUnderflowException,
@@ -258,6 +260,12 @@
}
@Override
+ short[] getValue() {
+ /* Not yet implemented */
+ return null;
+ }
+
+ @Override
public String toString() {
final StringJoiner valueJoiner = new StringJoiner(",");
for (short key : mValue) {
@@ -267,7 +275,7 @@
}
}
- private static class SvcParamAlpn extends SvcParam {
+ private static class SvcParamAlpn extends SvcParam<List<String>> {
private final List<String> mValue;
SvcParamAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
@@ -281,6 +289,7 @@
}
}
+ @Override
List<String> getValue() {
return Collections.unmodifiableList(mValue);
}
@@ -291,7 +300,7 @@
}
}
- private static class SvcParamNoDefaultAlpn extends SvcParam {
+ private static class SvcParamNoDefaultAlpn extends SvcParam<Void> {
SvcParamNoDefaultAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException,
ParseException {
super(KEY_NO_DEFAULT_ALPN);
@@ -303,12 +312,17 @@
}
@Override
+ Void getValue() {
+ return null;
+ }
+
+ @Override
public String toString() {
return toKeyName(getKey());
}
}
- private static class SvcParamPort extends SvcParam {
+ private static class SvcParamPort extends SvcParam<Integer> {
private final int mValue;
SvcParamPort(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
@@ -321,7 +335,8 @@
mValue = Short.toUnsignedInt(buf.getShort());
}
- int getValue() {
+ @Override
+ Integer getValue() {
return mValue;
}
@@ -331,7 +346,7 @@
}
}
- private static class SvcParamIpHint extends SvcParam {
+ private static class SvcParamIpHint extends SvcParam<List<InetAddress>> {
private final List<InetAddress> mValue;
private SvcParamIpHint(int key, @NonNull ByteBuffer buf, int addrLen) throws
@@ -346,6 +361,7 @@
}
}
+ @Override
List<InetAddress> getValue() {
return Collections.unmodifiableList(mValue);
}
@@ -378,7 +394,7 @@
}
}
- private static class SvcParamDohPath extends SvcParam {
+ private static class SvcParamDohPath extends SvcParam<String> {
private final String mValue;
SvcParamDohPath(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
@@ -390,6 +406,7 @@
mValue = new String(value, StandardCharsets.UTF_8);
}
+ @Override
String getValue() {
return mValue;
}
@@ -401,7 +418,7 @@
}
// For other unrecognized and unimplemented SvcParams, they are stored as SvcParamGeneric.
- private static class SvcParamGeneric extends SvcParam {
+ private static class SvcParamGeneric extends SvcParam<byte[]> {
private final byte[] mValue;
SvcParamGeneric(int key, @NonNull ByteBuffer buf) throws BufferUnderflowException,
@@ -414,6 +431,12 @@
}
@Override
+ byte[] getValue() {
+ /* Not yet implemented */
+ return null;
+ }
+
+ @Override
public String toString() {
final StringBuilder out = new StringBuilder();
out.append(toKeyName(getKey()));
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
index 6778f8a..d59795f 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
@@ -207,7 +207,7 @@
os.write(shortToByteArray((short) mRdataLen));
} else {
final byte[] targetNameLabels =
- DnsPacketUtils.DnsRecordParser.domainNameToLabels(mTargetName);
+ DnsPacketUtils.DnsRecordParser.domainNameToLabels(mTargetName);
mRdataLen += (Short.BYTES + targetNameLabels.length);
os.write(shortToByteArray((short) mRdataLen));
os.write(shortToByteArray(mSvcPriority));
@@ -251,7 +251,9 @@
// Check the content returned from toString() for now because the getter function for
// this SvcParam hasn't been implemented.
// TODO(b/240259333): Consider adding DnsSvcbRecord.isMandatory(String alpn) when needed.
- assertTrue(record.toString().contains("mandatory=ipv4hint,alpn,key333"));
+ assertTrue(record.toString().contains("ipv4hint"));
+ assertTrue(record.toString().contains("alpn"));
+ assertTrue(record.toString().contains("key333"));
}
@Test
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index 2d281fd..1ba83ca 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -19,6 +19,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import java.lang.IllegalStateException
import java.lang.reflect.Modifier
import org.junit.runner.Description
import org.junit.runner.Runner
@@ -27,6 +28,7 @@
import org.junit.runner.manipulation.NoTestsRemainException
import org.junit.runner.manipulation.Sortable
import org.junit.runner.manipulation.Sorter
+import org.junit.runner.notification.Failure
import org.junit.runner.notification.RunNotifier
import org.junit.runners.Parameterized
@@ -52,6 +54,9 @@
* class MyTestClass { ... }
*/
class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, Sortable {
+ private val leakMonitorDesc = Description.createTestDescription(klass, "ThreadLeakMonitor")
+ private val shouldThreadLeakFailTest = klass.isAnnotationPresent(MonitorThreadLeak::class.java)
+
// Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the
// Java bytecode doesn't have a way to express this. Give this type a name by wrapping it.
private class RunnerWrapper<T>(private val wrapped: T) :
@@ -61,6 +66,10 @@
override fun run(notifier: RunNotifier?) = wrapped.run(notifier)
}
+ // Annotation for test classes to indicate the test runner should monitor thread leak.
+ // TODO(b/307693729): Remove this annotation and monitor thread leak by default.
+ annotation class MonitorThreadLeak
+
private val baseRunner: RunnerWrapper<*>? = klass.let {
val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
@@ -81,20 +90,52 @@
it.isAnnotationPresent(Parameterized.Parameters::class.java) }
override fun run(notifier: RunNotifier) {
- if (baseRunner != null) {
+ if (baseRunner == null) {
+ // Report a single, skipped placeholder test for this class, as the class is expected to
+ // report results when run. In practice runners that apply the Filterable implementation
+ // would see a NoTestsRemainException and not call the run method.
+ notifier.fireTestIgnored(
+ Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
+ return
+ }
+ if (!shouldThreadLeakFailTest) {
baseRunner.run(notifier)
return
}
- // Report a single, skipped placeholder test for this class, as the class is expected to
- // report results when run. In practice runners that apply the Filterable implementation
- // would see a NoTestsRemainException and not call the run method.
- notifier.fireTestIgnored(
- Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
+ // Dump threads as a baseline to monitor thread leaks.
+ val threadCountsBeforeTest = getAllThreadNameCounts()
+
+ baseRunner.run(notifier)
+
+ notifier.fireTestStarted(leakMonitorDesc)
+ val threadCountsAfterTest = getAllThreadNameCounts()
+ if (threadCountsBeforeTest != threadCountsAfterTest) {
+ notifier.fireTestFailure(Failure(leakMonitorDesc,
+ IllegalStateException("Expected threads: $threadCountsBeforeTest " +
+ "but got: $threadCountsAfterTest")))
+ }
+ notifier.fireTestFinished(leakMonitorDesc)
+ }
+
+ private fun getAllThreadNameCounts(): Map<String, Int> {
+ // Get the counts of threads in the group per name.
+ // Filter system thread groups.
+ return Thread.getAllStackTraces().keys
+ .filter { it.threadGroup?.name != "system" }
+ .groupingBy { it.name }.eachCount()
}
override fun getDescription(): Description {
- return baseRunner?.description ?: Description.createSuiteDescription(klass)
+ if (baseRunner == null) {
+ return Description.createSuiteDescription(klass)
+ }
+
+ return baseRunner.description.also {
+ if (shouldThreadLeakFailTest) {
+ it.addChild(leakMonitorDesc)
+ }
+ }
}
/**
@@ -102,7 +143,9 @@
*/
override fun testCount(): Int {
// When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
- return baseRunner?.testCount() ?: 1
+ if (baseRunner == null) return 1
+
+ return baseRunner.testCount() + if (shouldThreadLeakFailTest) 1 else 0
}
@Throws(NoTestsRemainException::class)
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index bb32052..198b009 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -62,6 +62,8 @@
import android.util.Log;
import android.util.Pair;
+import androidx.annotation.Nullable;
+
import com.android.compatibility.common.util.AmUtils;
import com.android.compatibility.common.util.BatteryUtils;
import com.android.compatibility.common.util.DeviceConfigStateHelper;
@@ -283,8 +285,30 @@
}
protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
+ assertBackgroundNetworkAccess(expectAllowed, null);
+ }
+
+ /**
+ * Asserts whether the active network is available or not for the background app. If the network
+ * is unavailable, also checks whether it is blocked by the expected error.
+ *
+ * @param expectAllowed expect background network access to be allowed or not.
+ * @param expectedUnavailableError the expected error when {@code expectAllowed} is false. It's
+ * meaningful only when the {@code expectAllowed} is 'false'.
+ * Throws an IllegalArgumentException when {@code expectAllowed}
+ * is true and this parameter is not null. When the
+ * {@code expectAllowed} is 'false' and this parameter is null,
+ * this function does not compare error type of the networking
+ * access failure.
+ */
+ protected void assertBackgroundNetworkAccess(boolean expectAllowed,
+ @Nullable final String expectedUnavailableError) throws Exception {
assertBackgroundState();
- assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */);
+ if (expectAllowed && expectedUnavailableError != null) {
+ throw new IllegalArgumentException("expectedUnavailableError is not null");
+ }
+ assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */,
+ expectedUnavailableError);
}
protected void assertForegroundNetworkAccess() throws Exception {
@@ -407,12 +431,17 @@
*/
private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn)
throws Exception {
+ assertNetworkAccess(expectAvailable, needScreenOn, null);
+ }
+
+ private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn,
+ @Nullable final String expectedUnavailableError) throws Exception {
final int maxTries = 5;
String error = null;
int timeoutMs = 500;
for (int i = 1; i <= maxTries; i++) {
- error = checkNetworkAccess(expectAvailable);
+ error = checkNetworkAccess(expectAvailable, expectedUnavailableError);
if (error == null) return;
@@ -479,12 +508,15 @@
*
* @return error message with the mismatch (or empty if assertion passed).
*/
- private String checkNetworkAccess(boolean expectAvailable) throws Exception {
+ private String checkNetworkAccess(boolean expectAvailable,
+ @Nullable final String expectedUnavailableError) throws Exception {
final String resultData = mServiceClient.checkNetworkStatus();
- return checkForAvailabilityInResultData(resultData, expectAvailable);
+ return checkForAvailabilityInResultData(resultData, expectAvailable,
+ expectedUnavailableError);
}
- private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable) {
+ private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable,
+ @Nullable final String expectedUnavailableError) {
if (resultData == null) {
assertNotNull("Network status from app2 is null", resultData);
}
@@ -516,6 +548,10 @@
if (expectedState != state || expectedDetailedState != detailedState) {
errors.append(String.format("Connection state mismatch: expected %s/%s, got %s/%s\n",
expectedState, expectedDetailedState, state, detailedState));
+ } else if (!expectAvailable && (expectedUnavailableError != null)
+ && !connectionCheckDetails.contains(expectedUnavailableError)) {
+ errors.append("Connection unavailable reason mismatch: expected "
+ + expectedUnavailableError + "\n");
}
if (errors.length() > 0) {
@@ -914,7 +950,7 @@
final String resultData = result.get(0).second;
if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
final String error = checkForAvailabilityInResultData(
- resultData, expectAvailable);
+ resultData, expectAvailable, null /* expectedUnavailableError */);
if (error != null) {
fail("Network is not available for activity in app2 (" + mUid + "): "
+ error);
@@ -949,7 +985,7 @@
final String resultData = result.get(0).second;
if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
final String error = checkForAvailabilityInResultData(
- resultData, expectAvailable);
+ resultData, expectAvailable, null /* expectedUnavailableError */);
if (error != null) {
Log.d(TAG, "Network state is unexpected, checking again. " + error);
// Right now we could end up in an unexpected state if expedited job
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index ab3cf14..82f4a65 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -32,8 +32,11 @@
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
+import android.net.cts.util.CtsNetUtils;
import android.util.Log;
+import com.android.modules.utils.build.SdkLevel;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -46,6 +49,9 @@
public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCase {
private Network mNetwork;
private final TestNetworkCallback mTestNetworkCallback = new TestNetworkCallback();
+ private CtsNetUtils mCtsNetUtils;
+ private static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
+
@Rule
public final MeterednessConfigurationRule mMeterednessConfiguration
= new MeterednessConfigurationRule();
@@ -218,6 +224,26 @@
mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
false /* hasCapability */, NET_CAPABILITY_NOT_METERED);
mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+
+ // Before Android T, DNS queries over private DNS should be but are not restricted by Power
+ // Saver or Data Saver. The issue is fixed in mainline update and apps can no longer request
+ // DNS queries when its network is restricted by Power Saver. The fix takes effect backwards
+ // starting from Android T. But for Data Saver, the fix is not backward compatible since
+ // there are some platform changes involved. It is only available on devices that a specific
+ // trunk flag is enabled.
+ //
+ // This test can not only verify that the network traffic from apps is blocked at the right
+ // time, but also verify whether it is correctly blocked at the DNS stage, or at a later
+ // socket connection stage.
+ if (SdkLevel.isAtLeastT()) {
+ // Enable private DNS
+ mCtsNetUtils = new CtsNetUtils(mContext);
+ mCtsNetUtils.storePrivateDnsSetting();
+ mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
+ mCtsNetUtils.awaitPrivateDnsSetting(
+ "NetworkCallbackTest wait private DNS setting timeout", mNetwork,
+ GOOGLE_PRIVATE_DNS_SERVER, true);
+ }
}
@After
@@ -227,6 +253,10 @@
setRestrictBackground(false);
setBatterySaverMode(false);
unregisterNetworkCallback();
+
+ if (SdkLevel.isAtLeastT() && (mCtsNetUtils != null)) {
+ mCtsNetUtils.restorePrivateDnsSetting();
+ }
}
@RequiredProperties({DATA_SAVER_MODE})
@@ -235,6 +265,8 @@
try {
// Enable restrict background
setRestrictBackground(true);
+ // TODO: Verify expectedUnavailableError when aconfig support mainline.
+ // (see go/aconfig-in-mainline-problems)
assertBackgroundNetworkAccess(false);
assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
@@ -247,6 +279,7 @@
// Remove from whitelist
removeRestrictBackgroundWhitelist(mUid);
+ // TODO: Verify expectedUnavailableError when aconfig support mainline.
assertBackgroundNetworkAccess(false);
assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
@@ -278,7 +311,11 @@
try {
// Enable Power Saver
setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
+ if (SdkLevel.isAtLeastT()) {
+ assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+ } else {
+ assertBackgroundNetworkAccess(false);
+ }
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
@@ -298,7 +335,11 @@
try {
// Enable Power Saver
setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
+ if (SdkLevel.isAtLeastT()) {
+ assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+ } else {
+ assertBackgroundNetworkAccess(false);
+ }
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index bafd450..b8cf08e 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -31,6 +31,7 @@
import static android.Manifest.permission.NETWORK_STACK;
import static android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.STATUS_BAR_SERVICE;
import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -154,9 +155,12 @@
import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS;
import static android.os.Process.INVALID_UID;
import static android.system.OsConstants.IPPROTO_TCP;
+import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
+import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
import static com.android.server.ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION;
import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+import static com.android.server.ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS;
import static com.android.server.ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION;
import static com.android.server.ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED;
@@ -638,8 +642,8 @@
// BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
// underlying binder calls.
- final BatteryStatsManager mBatteryStatsManager =
- new BatteryStatsManager(mock(IBatteryStats.class));
+ final IBatteryStats mIBatteryStats = mock(IBatteryStats.class);
+ final BatteryStatsManager mBatteryStatsManager = new BatteryStatsManager(mIBatteryStats);
private ArgumentCaptor<ResolverParamsParcel> mResolverParamsParcelCaptor =
ArgumentCaptor.forClass(ResolverParamsParcel.class);
@@ -2150,6 +2154,16 @@
}
}
+ @Override
+ public boolean isFeatureNotChickenedOut(Context context, String name) {
+ switch (name) {
+ case ALLOW_SYSUI_CONNECTIVITY_REPORTS:
+ return true;
+ default:
+ return super.isFeatureNotChickenedOut(context, name);
+ }
+ }
+
public void setChangeIdEnabled(final boolean enabled, final long changeId, final int uid) {
final Pair<Long, Integer> data = new Pair<>(changeId, uid);
// mEnabledChangeIds is read on the handler thread and maybe the test thread, so
@@ -10795,6 +10809,11 @@
expectNativeNetworkCreated(netId, permission, iface, null /* inOrder */);
}
+ private int getIdleTimerLabel(int netId, int transportType) {
+ return ConnectivityService.LegacyNetworkActivityTracker.getIdleTimerLabel(
+ mDeps.isAtLeastV(), netId, transportType);
+ }
+
@Test
public void testStackedLinkProperties() throws Exception {
final LinkAddress myIpv4 = new LinkAddress("1.2.3.4/24");
@@ -11036,7 +11055,7 @@
networkCallback.expect(LOST, mCellAgent);
networkCallback.assertNoCallback();
verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(Integer.toString(getIdleTimerLabel(cellNetId, TRANSPORT_CELLULAR))));
verify(mMockNetd).networkDestroy(cellNetId);
if (mDeps.isAtLeastU()) {
verify(mMockNetd).setNetworkAllowlist(any());
@@ -11095,7 +11114,7 @@
}
verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(Integer.toString(getIdleTimerLabel(cellNetId, TRANSPORT_CELLULAR))));
verify(mMockNetd).networkDestroy(cellNetId);
if (mDeps.isAtLeastU()) {
verify(mMockNetd).setNetworkAllowlist(any());
@@ -11350,8 +11369,21 @@
final ConditionVariable onNetworkActiveCv = new ConditionVariable();
final ConnectivityManager.OnNetworkActiveListener listener = onNetworkActiveCv::open;
+ TestNetworkCallback defaultCallback = new TestNetworkCallback();
+
testAndCleanup(() -> {
+ mCm.registerDefaultNetworkCallback(defaultCallback);
agent.connect(true);
+ defaultCallback.expectAvailableThenValidatedCallbacks(agent);
+ if (transportType == TRANSPORT_CELLULAR) {
+ verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+ } else if (transportType == TRANSPORT_WIFI) {
+ verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+ }
+ clearInvocations(mIBatteryStats);
+ final int idleTimerLabel = getIdleTimerLabel(agent.getNetwork().netId, transportType);
// Network is considered active when the network becomes the default network.
assertTrue(mCm.isDefaultNetworkActive());
@@ -11360,19 +11392,57 @@
// Interface goes to inactive state
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
- transportType, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+ idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
mServiceContext.expectDataActivityBroadcast(legacyType, false /* isActive */,
TIMESTAMP);
assertFalse(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
assertFalse(mCm.isDefaultNetworkActive());
+ if (mDeps.isAtLeastV()) {
+ if (transportType == TRANSPORT_CELLULAR) {
+ verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_LOW),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+ } else if (transportType == TRANSPORT_WIFI) {
+ verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_LOW),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+ }
+ } else {
+ // If TrackMultiNetworks is disabled, LegacyNetworkActivityTracker does not call
+ // BatteryStats API by the netd activity change callback since BatteryStatsService
+ // listen to netd callback via NetworkManagementService and update battery stats by
+ // itself.
+ verify(mIBatteryStats, never())
+ .noteMobileRadioPowerState(anyInt(), anyLong(), anyInt());
+ verify(mIBatteryStats, never())
+ .noteWifiRadioPowerState(anyInt(), anyLong(), anyInt());
+ }
// Interface goes to active state
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
- transportType, TIMESTAMP, TEST_PACKAGE_UID);
+ idleTimerLabel, TIMESTAMP, TEST_PACKAGE_UID);
mServiceContext.expectDataActivityBroadcast(legacyType, true /* isActive */, TIMESTAMP);
assertTrue(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
assertTrue(mCm.isDefaultNetworkActive());
+ if (mDeps.isAtLeastV()) {
+ if (transportType == TRANSPORT_CELLULAR) {
+ verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(TEST_PACKAGE_UID));
+ } else if (transportType == TRANSPORT_WIFI) {
+ verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(TEST_PACKAGE_UID));
+ }
+ } else {
+ // If TrackMultiNetworks is disabled, LegacyNetworkActivityTracker does not call
+ // BatteryStats API by the netd activity change callback since BatteryStatsService
+ // listen to netd callback via NetworkManagementService and update battery stats by
+ // itself.
+ verify(mIBatteryStats, never())
+ .noteMobileRadioPowerState(anyInt(), anyLong(), anyInt());
+ verify(mIBatteryStats, never())
+ .noteWifiRadioPowerState(anyInt(), anyLong(), anyInt());
+ }
}, () -> { // Cleanup
+ mCm.unregisterNetworkCallback(defaultCallback);
+ }, () -> { // Cleanup
mCm.removeDefaultNetworkActiveListener(listener);
}, () -> { // Cleanup
agent.disconnect();
@@ -11420,12 +11490,13 @@
}
@Test
- public void testOnNetworkActive_NewEthernetConnects_CallbackNotCalled() throws Exception {
- // LegacyNetworkActivityTracker calls onNetworkActive callback only for networks that
- // tracker adds the idle timer to. And the tracker does not set the idle timer for the
- // ethernet network.
+ public void testOnNetworkActive_NewEthernetConnects_Callback() throws Exception {
+ // On pre-V devices, LegacyNetworkActivityTracker calls onNetworkActive callback only for
+ // networks that tracker adds the idle timer to. And the tracker does not set the idle timer
+ // for the ethernet network.
// So onNetworkActive is not called when the ethernet becomes the default network
- doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, false /* expectCallback */);
+ final boolean expectCallback = mDeps.isAtLeastV();
+ doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCallback);
}
@Test
@@ -11455,15 +11526,19 @@
mCm.registerNetworkCallback(networkRequest, networkCallback);
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+ final String cellIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+ mCellAgent.getNetwork().netId, TRANSPORT_CELLULAR));
final LinkProperties cellLp = new LinkProperties();
cellLp.setInterfaceName(MOBILE_IFNAME);
mCellAgent.sendLinkProperties(cellLp);
mCellAgent.connect(true);
networkCallback.expectAvailableThenValidatedCallbacks(mCellAgent);
verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(cellIdleTimerLabel));
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+ String wifiIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+ mWiFiAgent.getNetwork().netId, TRANSPORT_WIFI));
final LinkProperties wifiLp = new LinkProperties();
wifiLp.setInterfaceName(WIFI_IFNAME);
mWiFiAgent.sendLinkProperties(wifiLp);
@@ -11474,9 +11549,18 @@
networkCallback.expectLosing(mCellAgent);
networkCallback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_WIFI)));
- verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(wifiIdleTimerLabel));
+ if (mDeps.isAtLeastV()) {
+ // V+ devices add idleTimer when the network is first connected and remove when the
+ // network is disconnected.
+ verify(mMockNetd, never()).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(mCellAgent.getNetwork().netId)));
+ } else {
+ // pre V devices add idleTimer when the network becomes the default network and remove
+ // when the network becomes no longer the default network.
+ verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(TRANSPORT_CELLULAR)));
+ }
// Disconnect wifi and switch back to cell
reset(mMockNetd);
@@ -11484,13 +11568,20 @@
networkCallback.expect(LOST, mWiFiAgent);
assertNoCallbacks(networkCallback);
verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_WIFI)));
- verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(wifiIdleTimerLabel));
+ if (mDeps.isAtLeastV()) {
+ verify(mMockNetd, never()).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(mCellAgent.getNetwork().netId)));
+ } else {
+ verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(TRANSPORT_CELLULAR)));
+ }
// reconnect wifi
reset(mMockNetd);
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+ wifiIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+ mWiFiAgent.getNetwork().netId, TRANSPORT_WIFI));
wifiLp.setInterfaceName(WIFI_IFNAME);
mWiFiAgent.sendLinkProperties(wifiLp);
mWiFiAgent.connect(true);
@@ -11498,20 +11589,30 @@
networkCallback.expectLosing(mCellAgent);
networkCallback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_WIFI)));
- verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(wifiIdleTimerLabel));
+ if (mDeps.isAtLeastV()) {
+ verify(mMockNetd, never()).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(mCellAgent.getNetwork().netId)));
+ } else {
+ verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(TRANSPORT_CELLULAR)));
+ }
// Disconnect cell
reset(mMockNetd);
mCellAgent.disconnect();
networkCallback.expect(LOST, mCellAgent);
- // LOST callback is triggered earlier than removing idle timer. Broadcast should also be
- // sent as network being switched. Ensure rule removal for cell will not be triggered
- // unexpectedly before network being removed.
waitForIdle();
- verify(mMockNetd, times(0)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ if (mDeps.isAtLeastV()) {
+ verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(mCellAgent.getNetwork().netId)));
+ } else {
+ // LOST callback is triggered earlier than removing idle timer. Broadcast should also be
+ // sent as network being switched. Ensure rule removal for cell will not be triggered
+ // unexpectedly before network being removed.
+ verify(mMockNetd, times(0)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(TRANSPORT_CELLULAR)));
+ }
verify(mMockNetd, times(1)).networkDestroy(eq(mCellAgent.getNetwork().netId));
verify(mMockDnsResolver, times(1)).destroyNetworkCache(eq(mCellAgent.getNetwork().netId));
@@ -11520,12 +11621,27 @@
mWiFiAgent.disconnect();
b.expectBroadcast();
verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_WIFI)));
+ eq(wifiIdleTimerLabel));
// Clean up
mCm.unregisterNetworkCallback(networkCallback);
}
+ @Test
+ public void testDataActivityTracking_VpnNetwork() throws Exception {
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+ mWiFiAgent.connect(true /* validated */);
+ mMockVpn.setUnderlyingNetworks(new Network[] { mWiFiAgent.getNetwork() });
+
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(VPN_IFNAME);
+ mMockVpn.establishForMyUid(lp);
+
+ // NetworkActivityTracker should not track the VPN network since VPN can change the
+ // underlying network without disconnect.
+ verify(mMockNetd, never()).idletimerAddInterface(eq(VPN_IFNAME), anyInt(), any());
+ }
+
private void verifyTcpBufferSizeChange(String tcpBufferSizes) throws Exception {
String[] values = tcpBufferSizes.split(",");
String rmemValues = String.join(" ", values[0], values[1], values[2]);
@@ -12810,6 +12926,18 @@
}
@Test
+ public void testCheckConnectivityDiagnosticsPermissionsSysUi() throws Exception {
+ final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities());
+
+ mServiceContext.setPermission(STATUS_BAR_SERVICE, PERMISSION_GRANTED);
+ assertTrue(
+ "SysUi permission (STATUS_BAR_SERVICE) not applied",
+ mService.checkConnectivityDiagnosticsPermissions(
+ Process.myPid(), Process.myUid(), naiWithoutUid,
+ mContext.getOpPackageName()));
+ }
+
+ @Test
public void testCheckConnectivityDiagnosticsPermissionsWrongUidPackageName() throws Exception {
final int wrongUid = Process.myUid() + 1;
@@ -18704,6 +18832,7 @@
final LinkProperties lp = new LinkProperties();
lp.setInterfaceName(transportToTestIfaceName(transportType));
final TestNetworkAgentWrapper agent = new TestNetworkAgentWrapper(transportType, lp);
+ final int idleTimerLabel = getIdleTimerLabel(agent.getNetwork().netId, transportType);
testAndCleanup(() -> {
final UidFrozenStateChangedCallback uidFrozenStateChangedCallback =
getUidFrozenStateChangedCallback().get();
@@ -18716,7 +18845,7 @@
if (freezeWithNetworkInactive) {
// Make network inactive
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
- transportType, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+ idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
}
// Freeze TEST_FROZEN_UID and TEST_UNFROZEN_UID
@@ -18740,7 +18869,7 @@
// Make network active
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
- transportType, TIMESTAMP, TEST_PACKAGE_UID);
+ idleTimerLabel, TIMESTAMP, TEST_PACKAGE_UID);
waitForIdle();
if (expectDelay) {
@@ -18759,8 +18888,8 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testDelayFrozenUidSocketDestroy_ActiveCellular() throws Exception {
- doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR,
- false /* freezeWithNetworkInactive */, false /* expectDelay */);
+ doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR, false /* freezeWithNetworkInactive */,
+ false /* expectDelay */);
}
@Test
@@ -18768,22 +18897,22 @@
public void testDelayFrozenUidSocketDestroy_InactiveCellular() throws Exception {
// When the default network is cellular and cellular network is inactive, closing socket
// is delayed.
- doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR,
- true /* freezeWithNetworkInactive */, true /* expectDelay */);
+ doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR, true /* freezeWithNetworkInactive */,
+ true /* expectDelay */);
}
@Test
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testDelayFrozenUidSocketDestroy_ActiveWifi() throws Exception {
- doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI,
- false /* freezeWithNetworkInactive */, false /* expectDelay */);
+ doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI, false /* freezeWithNetworkInactive */,
+ false /* expectDelay */);
}
@Test
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testDelayFrozenUidSocketDestroy_InactiveWifi() throws Exception {
- doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI,
- true /* freezeWithNetworkInactive */, false /* expectDelay */);
+ doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI, true /* freezeWithNetworkInactive */,
+ false /* expectDelay */);
}
/**
@@ -18804,6 +18933,8 @@
final LinkProperties cellLp = new LinkProperties();
cellLp.setInterfaceName(MOBILE_IFNAME);
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+ final int idleTimerLabel =
+ getIdleTimerLabel(mCellAgent.getNetwork().netId, TRANSPORT_CELLULAR);
final TestNetworkCallback defaultCallback = new TestNetworkCallback();
mCm.registerDefaultNetworkCallback(defaultCallback);
@@ -18813,7 +18944,7 @@
// Make cell network inactive
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
- TRANSPORT_CELLULAR, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+ idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
// Freeze TEST_FROZEN_UID
final int[] uids = {TEST_FROZEN_UID};
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 48cfe77..ff801e5 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -56,11 +56,9 @@
import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV4_UDP;
import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_ESP;
import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_UDP;
-import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor;
import static com.android.testutils.MiscAsserts.assertThrows;
-import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -149,7 +147,6 @@
import android.net.wifi.WifiInfo;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
-import android.os.ConditionVariable;
import android.os.INetworkManagementService;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
@@ -1983,22 +1980,6 @@
// a subsequent CL.
}
- @Test
- public void testStartLegacyVpnIpv6() throws Exception {
- setMockedUsers(PRIMARY_USER);
- final Vpn vpn = createVpn(PRIMARY_USER.id);
- final LinkProperties lp = new LinkProperties();
- lp.setInterfaceName(EGRESS_IFACE);
- lp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
- final RouteInfo defaultRoute = new RouteInfo(
- new IpPrefix(Inet6Address.ANY, 0), null, EGRESS_IFACE);
- lp.addRoute(defaultRoute);
-
- // IllegalStateException thrown since legacy VPN only supports IPv4.
- assertThrows(IllegalStateException.class,
- () -> vpn.startLegacyVpn(mVpnProfile, EGRESS_NETWORK, lp));
- }
-
private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
setMockedUsers(PRIMARY_USER);
@@ -3112,23 +3093,15 @@
}
@Test
- public void testStartRacoonNumericAddress() throws Exception {
- startRacoon("1.2.3.4", "1.2.3.4");
- }
+ public void testStartLegacyVpnType() throws Exception {
+ setMockedUsers(PRIMARY_USER);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final VpnProfile profile = new VpnProfile("testProfile" /* key */);
- @Test
- public void testStartRacoonHostname() throws Exception {
- startRacoon("hostname", "5.6.7.8"); // address returned by deps.resolve
- }
-
- @Test
- public void testStartPptp() throws Exception {
- startPptp(true /* useMppe */);
- }
-
- @Test
- public void testStartPptp_NoMppe() throws Exception {
- startPptp(false /* useMppe */);
+ profile.type = VpnProfile.TYPE_PPTP;
+ assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
+ profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
+ assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
}
private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
@@ -3138,125 +3111,6 @@
assertEquals(type, ti.getType());
}
- private void startPptp(boolean useMppe) throws Exception {
- final VpnProfile profile = new VpnProfile("testProfile" /* key */);
- profile.type = VpnProfile.TYPE_PPTP;
- profile.name = "testProfileName";
- profile.username = "userName";
- profile.password = "thePassword";
- profile.server = "192.0.2.123";
- profile.mppe = useMppe;
-
- doReturn(new Network[] { new Network(101) }).when(mConnectivityManager).getAllNetworks();
- doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(
- any(), // INetworkAgent
- any(), // NetworkInfo
- any(), // LinkProperties
- any(), // NetworkCapabilities
- any(), // LocalNetworkConfig
- any(), // NetworkScore
- any(), // NetworkAgentConfig
- anyInt()); // provider ID
-
- final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
- final TestDeps deps = (TestDeps) vpn.mDeps;
-
- testAndCleanup(() -> {
- final String[] mtpdArgs = deps.mtpdArgs.get(10, TimeUnit.SECONDS);
- final String[] argsPrefix = new String[]{
- EGRESS_IFACE, "pptp", profile.server, "1723", "name", profile.username,
- "password", profile.password, "linkname", "vpn", "refuse-eap", "nodefaultroute",
- "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270"
- };
- assertArrayEquals(argsPrefix, Arrays.copyOf(mtpdArgs, argsPrefix.length));
- if (useMppe) {
- assertEquals(argsPrefix.length + 2, mtpdArgs.length);
- assertEquals("+mppe", mtpdArgs[argsPrefix.length]);
- assertEquals("-pap", mtpdArgs[argsPrefix.length + 1]);
- } else {
- assertEquals(argsPrefix.length + 1, mtpdArgs.length);
- assertEquals("nomppe", mtpdArgs[argsPrefix.length]);
- }
-
- verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(
- any(), // INetworkAgent
- any(), // NetworkInfo
- any(), // LinkProperties
- any(), // NetworkCapabilities
- any(), // LocalNetworkConfig
- any(), // NetworkScore
- any(), // NetworkAgentConfig
- anyInt()); // provider ID
- }, () -> { // Cleanup
- vpn.mVpnRunner.exitVpnRunner();
- deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
- vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
- });
- }
-
- public void startRacoon(final String serverAddr, final String expectedAddr)
- throws Exception {
- final ConditionVariable legacyRunnerReady = new ConditionVariable();
- final VpnProfile profile = new VpnProfile("testProfile" /* key */);
- profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
- profile.name = "testProfileName";
- profile.username = "userName";
- profile.password = "thePassword";
- profile.server = serverAddr;
- profile.ipsecIdentifier = "id";
- profile.ipsecSecret = "secret";
- profile.l2tpSecret = "l2tpsecret";
-
- when(mConnectivityManager.getAllNetworks())
- .thenReturn(new Network[] { new Network(101) });
-
- when(mConnectivityManager.registerNetworkAgent(any(), any(), any(), any(),
- any(), any(), any(), anyInt())).thenAnswer(invocation -> {
- // The runner has registered an agent and is now ready.
- legacyRunnerReady.open();
- return new Network(102);
- });
- final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
- final TestDeps deps = (TestDeps) vpn.mDeps;
- try {
- // udppsk and 1701 are the values for TYPE_L2TP_IPSEC_PSK
- assertArrayEquals(
- new String[] { EGRESS_IFACE, expectedAddr, "udppsk",
- profile.ipsecIdentifier, profile.ipsecSecret, "1701" },
- deps.racoonArgs.get(10, TimeUnit.SECONDS));
- // literal values are hardcoded in Vpn.java for mtpd args
- assertArrayEquals(
- new String[] { EGRESS_IFACE, "l2tp", expectedAddr, "1701", profile.l2tpSecret,
- "name", profile.username, "password", profile.password,
- "linkname", "vpn", "refuse-eap", "nodefaultroute", "usepeerdns",
- "idle", "1800", "mtu", "1270", "mru", "1270" },
- deps.mtpdArgs.get(10, TimeUnit.SECONDS));
-
- // Now wait for the runner to be ready before testing for the route.
- ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
- ArgumentCaptor<NetworkCapabilities> ncCaptor =
- ArgumentCaptor.forClass(NetworkCapabilities.class);
- verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
- lpCaptor.capture(), ncCaptor.capture(), any(), any(), any(), anyInt());
-
- // In this test the expected address is always v4 so /32.
- // Note that the interface needs to be specified because RouteInfo objects stored in
- // LinkProperties objects always acquire the LinkProperties' interface.
- final RouteInfo expectedRoute = new RouteInfo(new IpPrefix(expectedAddr + "/32"),
- null, EGRESS_IFACE, RouteInfo.RTN_THROW);
- final List<RouteInfo> actualRoutes = lpCaptor.getValue().getRoutes();
- assertTrue("Expected throw route (" + expectedRoute + ") not found in " + actualRoutes,
- actualRoutes.contains(expectedRoute));
-
- assertTransportInfoMatches(ncCaptor.getValue(), VpnManager.TYPE_VPN_LEGACY);
- } finally {
- // Now interrupt the thread, unblock the runner and clean up.
- vpn.mVpnRunner.exitVpnRunner();
- deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
- vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
- }
- }
-
// Make it public and un-final so as to spy it
public class TestDeps extends Vpn.Dependencies {
public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index 2b3b834..3cea5cb 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -19,7 +19,10 @@
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
+import com.android.net.module.util.ArrayTrackRecord
import com.android.server.connectivity.mdns.MdnsServiceCache.CacheKey
+import com.android.server.connectivity.mdns.MdnsServiceCacheTest.ExpiredRecord.ExpiredEvent.ServiceRecordExpired
+import com.android.server.connectivity.mdns.util.MdnsUtils
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import java.util.concurrent.CompletableFuture
@@ -32,13 +35,19 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
private const val SERVICE_NAME_1 = "service-instance-1"
private const val SERVICE_NAME_2 = "service-instance-2"
+private const val SERVICE_NAME_3 = "service-instance-3"
private const val SERVICE_TYPE_1 = "_test1._tcp.local"
private const val SERVICE_TYPE_2 = "_test2._tcp.local"
private const val INTERFACE_INDEX = 999
private const val DEFAULT_TIMEOUT_MS = 2000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
+private const val TEST_ELAPSED_REALTIME_MS = 123L
+private const val DEFAULT_TTL_TIME_MS = 120000L
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -47,10 +56,46 @@
private val cacheKey1 = CacheKey(SERVICE_TYPE_1, socketKey)
private val cacheKey2 = CacheKey(SERVICE_TYPE_2, socketKey)
private val thread = HandlerThread(MdnsServiceCacheTest::class.simpleName)
+ private val clock = mock(MdnsUtils.Clock::class.java)
private val handler by lazy {
Handler(thread.looper)
}
+ private class ExpiredRecord : MdnsServiceCache.ServiceExpiredCallback {
+ val history = ArrayTrackRecord<ExpiredEvent>().newReadHead()
+
+ sealed class ExpiredEvent {
+ abstract val previousResponse: MdnsResponse
+ abstract val newResponse: MdnsResponse?
+ data class ServiceRecordExpired(
+ override val previousResponse: MdnsResponse,
+ override val newResponse: MdnsResponse?
+ ) : ExpiredEvent()
+ }
+
+ override fun onServiceRecordExpired(
+ previousResponse: MdnsResponse,
+ newResponse: MdnsResponse?
+ ) {
+ history.add(ServiceRecordExpired(previousResponse, newResponse))
+ }
+
+ fun expectedServiceRecordExpired(
+ serviceName: String,
+ timeoutMs: Long = DEFAULT_TIMEOUT_MS
+ ) {
+ val event = history.poll(timeoutMs)
+ assertNotNull(event)
+ assertTrue(event is ServiceRecordExpired)
+ assertEquals(serviceName, event.previousResponse.serviceInstanceName)
+ }
+
+ fun assertNoCallback() {
+ val cb = history.poll(NO_CALLBACK_TIMEOUT_MS)
+ assertNull("Expected no callback but got $cb", cb)
+ }
+ }
+
@Before
fun setUp() {
thread.start()
@@ -89,19 +134,27 @@
private fun getService(
serviceCache: MdnsServiceCache,
serviceName: String,
- cacheKey: CacheKey,
+ cacheKey: CacheKey
): MdnsResponse? = runningOnHandlerAndReturn {
serviceCache.getCachedService(serviceName, cacheKey)
}
private fun getServices(
serviceCache: MdnsServiceCache,
- cacheKey: CacheKey,
+ cacheKey: CacheKey
): List<MdnsResponse> = runningOnHandlerAndReturn { serviceCache.getCachedServices(cacheKey) }
+ private fun registerServiceExpiredCallback(
+ serviceCache: MdnsServiceCache,
+ cacheKey: CacheKey,
+ callback: MdnsServiceCache.ServiceExpiredCallback
+ ) = runningOnHandlerAndReturn {
+ serviceCache.registerServiceExpiredCallback(cacheKey, callback)
+ }
+
@Test
fun testAddAndRemoveService() {
- val serviceCache = MdnsServiceCache(thread.looper, makeFlags())
+ val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
var response = getService(serviceCache, SERVICE_NAME_1, cacheKey1)
assertNotNull(response)
@@ -113,7 +166,7 @@
@Test
fun testGetCachedServices_multipleServiceTypes() {
- val serviceCache = MdnsServiceCache(thread.looper, makeFlags())
+ val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_2, SERVICE_TYPE_2))
@@ -145,7 +198,127 @@
})
}
- private fun createResponse(serviceInstanceName: String, serviceType: String) = MdnsResponse(
- 0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
- socketKey.interfaceIndex, socketKey.network)
+ @Test
+ fun testServiceExpiredAndSendCallbacks() {
+ val serviceCache = MdnsServiceCache(
+ thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+ // Register service expired callbacks
+ val callback1 = ExpiredRecord()
+ val callback2 = ExpiredRecord()
+ registerServiceExpiredCallback(serviceCache, cacheKey1, callback1)
+ registerServiceExpiredCallback(serviceCache, cacheKey2, callback2)
+
+ doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
+
+ // Add multiple services with different ttl time.
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1,
+ DEFAULT_TTL_TIME_MS))
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1,
+ DEFAULT_TTL_TIME_MS + 20L))
+ addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_3, SERVICE_TYPE_2,
+ DEFAULT_TTL_TIME_MS + 10L))
+
+ // Check the service expiration immediately. Should be no callback.
+ assertEquals(2, getServices(serviceCache, cacheKey1).size)
+ assertEquals(1, getServices(serviceCache, cacheKey2).size)
+ callback1.assertNoCallback()
+ callback2.assertNoCallback()
+
+ // Simulate the case where the response is after TTL then check expired services.
+ // Expect SERVICE_NAME_1 expired.
+ doReturn(TEST_ELAPSED_REALTIME_MS + DEFAULT_TTL_TIME_MS).`when`(clock).elapsedRealtime()
+ assertEquals(1, getServices(serviceCache, cacheKey1).size)
+ assertEquals(1, getServices(serviceCache, cacheKey2).size)
+ callback1.expectedServiceRecordExpired(SERVICE_NAME_1)
+ callback2.assertNoCallback()
+
+ // Simulate the case where the response is after TTL then check expired services.
+ // Expect SERVICE_NAME_3 expired.
+ doReturn(TEST_ELAPSED_REALTIME_MS + DEFAULT_TTL_TIME_MS + 11L)
+ .`when`(clock).elapsedRealtime()
+ assertEquals(1, getServices(serviceCache, cacheKey1).size)
+ assertEquals(0, getServices(serviceCache, cacheKey2).size)
+ callback1.assertNoCallback()
+ callback2.expectedServiceRecordExpired(SERVICE_NAME_3)
+ }
+
+ @Test
+ fun testRemoveExpiredServiceWhenGetting() {
+ val serviceCache = MdnsServiceCache(
+ thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+
+ doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
+ addOrUpdateService(serviceCache, cacheKey1,
+ createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 1L /* ttlTime */))
+ doReturn(TEST_ELAPSED_REALTIME_MS + 2L).`when`(clock).elapsedRealtime()
+ assertNull(getService(serviceCache, SERVICE_NAME_1, cacheKey1))
+
+ addOrUpdateService(serviceCache, cacheKey2,
+ createResponse(SERVICE_NAME_2, SERVICE_TYPE_2, 3L /* ttlTime */))
+ doReturn(TEST_ELAPSED_REALTIME_MS + 4L).`when`(clock).elapsedRealtime()
+ assertEquals(0, getServices(serviceCache, cacheKey2).size)
+ }
+
+ @Test
+ fun testInsertResponseAndSortList() {
+ val responses = ArrayList<MdnsResponse>()
+ val response1 = createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 100L /* ttlTime */)
+ MdnsServiceCache.insertResponseAndSortList(responses, response1, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(1, responses.size)
+ assertEquals(response1, responses[0])
+
+ val response2 = createResponse(SERVICE_NAME_2, SERVICE_TYPE_1, 50L /* ttlTime */)
+ MdnsServiceCache.insertResponseAndSortList(responses, response2, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(2, responses.size)
+ assertEquals(response2, responses[0])
+ assertEquals(response1, responses[1])
+
+ val response3 = createResponse(SERVICE_NAME_3, SERVICE_TYPE_1, 75L /* ttlTime */)
+ MdnsServiceCache.insertResponseAndSortList(responses, response3, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(3, responses.size)
+ assertEquals(response2, responses[0])
+ assertEquals(response3, responses[1])
+ assertEquals(response1, responses[2])
+
+ val response4 = createResponse("service-instance-4", SERVICE_TYPE_1, 125L /* ttlTime */)
+ MdnsServiceCache.insertResponseAndSortList(responses, response4, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(4, responses.size)
+ assertEquals(response2, responses[0])
+ assertEquals(response3, responses[1])
+ assertEquals(response1, responses[2])
+ assertEquals(response4, responses[3])
+ }
+
+ private fun createResponse(
+ serviceInstanceName: String,
+ serviceType: String,
+ ttlTime: Long = 120000L
+ ): MdnsResponse {
+ val serviceName = "$serviceInstanceName.$serviceType".split(".").toTypedArray()
+ val response = MdnsResponse(
+ 0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
+ socketKey.interfaceIndex, socketKey.network)
+
+ // Set PTR record
+ val pointerRecord = MdnsPointerRecord(
+ serviceType.split(".").toTypedArray(),
+ TEST_ELAPSED_REALTIME_MS /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ ttlTime /* ttlMillis */,
+ serviceName)
+ response.addPointerRecord(pointerRecord)
+
+ // Set SRV record.
+ val serviceRecord = MdnsServiceRecord(
+ serviceName,
+ TEST_ELAPSED_REALTIME_MS /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ ttlTime /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ 12345 /* port */,
+ arrayOf("hostname"))
+ response.serviceRecord = serviceRecord
+ return response
+ }
}
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 ce154dd..26a3796 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -194,7 +194,9 @@
thread.start();
handler = new Handler(thread.getLooper());
serviceCache = new MdnsServiceCache(
- thread.getLooper(), MdnsFeatureFlags.newBuilder().build());
+ thread.getLooper(),
+ MdnsFeatureFlags.newBuilder().setIsExpiredServicesRemovalEnabled(false).build(),
+ mockDecoderClock);
doAnswer(inv -> {
latestDelayMs = 0;
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
new file mode 100644
index 0000000..526ec9d
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2023 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
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.ACTION_DATA_ACTIVITY_CHANGE
+import android.net.ConnectivityManager.EXTRA_DEVICE_TYPE
+import android.net.ConnectivityManager.EXTRA_IS_ACTIVE
+import android.net.ConnectivityManager.EXTRA_REALTIME_NS
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_IMS
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.ConditionVariable
+import android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH
+import android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW
+import androidx.test.filters.SmallTest
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener
+import com.android.server.CSTest.CSContext
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertNotNull
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+private const val DATA_CELL_IFNAME = "rmnet_data"
+private const val IMS_CELL_IFNAME = "rmnet_ims"
+private const val WIFI_IFNAME = "wlan0"
+private const val TIMESTAMP = 1234L
+private const val NETWORK_ACTIVITY_NO_UID = -1
+private const val PACKAGE_UID = 123
+private const val TIMEOUT_MS = 250L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class CSNetworkActivityTest : CSTest() {
+
+ private fun getRegisteredNetdUnsolicitedEventListener(): BaseNetdUnsolicitedEventListener {
+ val captor = ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener::class.java)
+ verify(netd).registerUnsolicitedEventListener(captor.capture())
+ return captor.value
+ }
+
+ @Test
+ fun testInterfaceClassActivityChanged_NonDefaultNetwork() {
+ val netdUnsolicitedEventListener = getRegisteredNetdUnsolicitedEventListener()
+ val batteryStatsInorder = inOrder(batteryStats)
+
+ val cellNr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+ val cellCb = TestableNetworkCallback()
+ // Request cell network to keep cell network up
+ cm.requestNetwork(cellNr, cellCb)
+
+ val defaultCb = TestableNetworkCallback()
+ cm.registerDefaultNetworkCallback(defaultCb)
+
+ val cellNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val cellLp = LinkProperties().apply {
+ interfaceName = DATA_CELL_IFNAME
+ }
+ // Connect Cellular network
+ val cellAgent = Agent(nc = cellNc, lp = cellLp)
+ cellAgent.connect()
+ defaultCb.expectAvailableCallbacks(cellAgent.network, validated = false)
+
+ val wifiNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val wifiLp = LinkProperties().apply {
+ interfaceName = WIFI_IFNAME
+ }
+ // Connect Wi-Fi network, Wi-Fi network should be the default network.
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ defaultCb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ batteryStatsInorder.verify(batteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+ val onNetworkActiveCv = ConditionVariable()
+ val listener = ConnectivityManager.OnNetworkActiveListener { onNetworkActiveCv::open }
+ cm.addDefaultNetworkActiveListener(listener)
+
+ // Cellular network (non default network) goes to inactive state.
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ cellAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+ // Non-default network activity change does not change default network activity
+ // But cellular radio power state is updated
+ assertFalse(onNetworkActiveCv.block(TIMEOUT_MS))
+ context.expectNoDataActivityBroadcast(0 /* timeoutMs */)
+ assertTrue(cm.isDefaultNetworkActive)
+ batteryStatsInorder.verify(batteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_LOW),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+ // Cellular network (non default network) goes to active state.
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+ cellAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+ // Non-default network activity change does not change default network activity
+ // But cellular radio power state is updated
+ assertFalse(onNetworkActiveCv.block(TIMEOUT_MS))
+ context.expectNoDataActivityBroadcast(0 /* timeoutMs */)
+ assertTrue(cm.isDefaultNetworkActive)
+ batteryStatsInorder.verify(batteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(PACKAGE_UID))
+
+ cm.unregisterNetworkCallback(cellCb)
+ cm.unregisterNetworkCallback(defaultCb)
+ cm.removeDefaultNetworkActiveListener(listener)
+ }
+
+ @Test
+ fun testDataActivityTracking_MultiCellNetwork() {
+ val netdUnsolicitedEventListener = getRegisteredNetdUnsolicitedEventListener()
+ val batteryStatsInorder = inOrder(batteryStats)
+
+ val dataNetworkNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .build()
+ val dataNetworkNr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+ val dataNetworkLp = LinkProperties().apply {
+ interfaceName = DATA_CELL_IFNAME
+ }
+ val dataNetworkCb = TestableNetworkCallback()
+ cm.requestNetwork(dataNetworkNr, dataNetworkCb)
+ val dataNetworkAgent = Agent(nc = dataNetworkNc, lp = dataNetworkLp)
+ val dataNetworkNetId = dataNetworkAgent.network.netId.toString()
+
+ val imsNetworkNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_IMS)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .build()
+ val imsNetworkNr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_IMS)
+ .build()
+ val imsNetworkLp = LinkProperties().apply {
+ interfaceName = IMS_CELL_IFNAME
+ }
+ val imsNetworkCb = TestableNetworkCallback()
+ cm.requestNetwork(imsNetworkNr, imsNetworkCb)
+ val imsNetworkAgent = Agent(nc = imsNetworkNc, lp = imsNetworkLp)
+ val imsNetworkNetId = imsNetworkAgent.network.netId.toString()
+
+ dataNetworkAgent.connect()
+ dataNetworkCb.expectAvailableCallbacks(dataNetworkAgent.network, validated = false)
+
+ imsNetworkAgent.connect()
+ imsNetworkCb.expectAvailableCallbacks(imsNetworkAgent.network, validated = false)
+
+ // Both cell networks have idleTimers
+ verify(netd).idletimerAddInterface(eq(DATA_CELL_IFNAME), anyInt(), eq(dataNetworkNetId))
+ verify(netd).idletimerAddInterface(eq(IMS_CELL_IFNAME), anyInt(), eq(imsNetworkNetId))
+ verify(netd, never()).idletimerRemoveInterface(eq(DATA_CELL_IFNAME), anyInt(),
+ eq(dataNetworkNetId))
+ verify(netd, never()).idletimerRemoveInterface(eq(IMS_CELL_IFNAME), anyInt(),
+ eq(imsNetworkNetId))
+
+ // Both cell networks go to inactive state
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ imsNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ dataNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+
+ // Data cell network goes to active state. This should update the cellular radio power state
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+ dataNetworkAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+ batteryStatsInorder.verify(batteryStats, timeout(TIMEOUT_MS)).noteMobileRadioPowerState(
+ eq(DC_POWER_STATE_HIGH), anyLong() /* timestampNs */, eq(PACKAGE_UID))
+ // Ims cell network goes to active state. But this should not update the cellular radio
+ // power state since cellular radio power state is already high
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+ imsNetworkAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+ waitForIdle()
+ batteryStatsInorder.verify(batteryStats, never()).noteMobileRadioPowerState(anyInt(),
+ anyLong() /* timestampNs */, anyInt())
+
+ // Data cell network goes to inactive state. But this should not update the cellular radio
+ // power state ims cell network is still active state
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ dataNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+ waitForIdle()
+ batteryStatsInorder.verify(batteryStats, never()).noteMobileRadioPowerState(anyInt(),
+ anyLong() /* timestampNs */, anyInt())
+
+ // Ims cell network goes to inactive state.
+ // This should update the cellular radio power state
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ imsNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+ batteryStatsInorder.verify(batteryStats, timeout(TIMEOUT_MS)).noteMobileRadioPowerState(
+ eq(DC_POWER_STATE_LOW), anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+ dataNetworkAgent.disconnect()
+ dataNetworkCb.expect<Lost>(dataNetworkAgent.network)
+ verify(netd).idletimerRemoveInterface(eq(DATA_CELL_IFNAME), anyInt(), eq(dataNetworkNetId))
+
+ imsNetworkAgent.disconnect()
+ imsNetworkCb.expect<Lost>(imsNetworkAgent.network)
+ verify(netd).idletimerRemoveInterface(eq(IMS_CELL_IFNAME), anyInt(), eq(imsNetworkNetId))
+
+ cm.unregisterNetworkCallback(dataNetworkCb)
+ cm.unregisterNetworkCallback(imsNetworkCb)
+ }
+}
+
+internal fun CSContext.expectDataActivityBroadcast(
+ deviceType: Int,
+ isActive: Boolean,
+ tsNanos: Long
+) {
+ assertNotNull(orderedBroadcastAsUserHistory.poll(BROADCAST_TIMEOUT_MS) {
+ intent -> intent.action.equals(ACTION_DATA_ACTIVITY_CHANGE) &&
+ intent.getIntExtra(EXTRA_DEVICE_TYPE, -1) == deviceType &&
+ intent.getBooleanExtra(EXTRA_IS_ACTIVE, !isActive) == isActive &&
+ intent.getLongExtra(EXTRA_REALTIME_NS, -1) == tsNanos
+ })
+}
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 0ccbfc3..f21a428 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -43,6 +43,7 @@
import android.net.PacProxyManager
import android.net.networkstack.NetworkStackClientBase
import android.os.BatteryStatsManager
+import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.os.UserHandle
@@ -54,6 +55,7 @@
import com.android.internal.app.IBatteryStats
import com.android.internal.util.test.BroadcastInterceptingContext
import com.android.modules.utils.build.SdkLevel
+import com.android.net.module.util.ArrayTrackRecord
import com.android.networkstack.apishim.common.UnsupportedApiLevelException
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker
import com.android.server.connectivity.CarrierPrivilegeAuthenticator
@@ -64,14 +66,16 @@
import com.android.server.connectivity.ProxyTracker
import com.android.testutils.visibleOnHandlerThread
import com.android.testutils.waitForIdle
+import java.util.concurrent.Executors
+import kotlin.test.assertNull
+import kotlin.test.fail
import org.mockito.AdditionalAnswers.delegatesTo
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
-import java.util.concurrent.Executors
-import kotlin.test.fail
internal const val HANDLER_TIMEOUT_MS = 2_000
+internal const val BROADCAST_TIMEOUT_MS = 3_000L
internal const val TEST_PACKAGE_NAME = "com.android.test.package"
internal const val WIFI_WOL_IFNAME = "test_wlan_wol"
internal val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
@@ -127,6 +131,7 @@
it[ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER] = true
it[ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION] = true
it[ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION] = true
+ it[ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS] = true
}
fun enableFeature(f: String) = enabledFeatures.set(f, true)
fun disableFeature(f: String) = enabledFeatures.set(f, false)
@@ -154,7 +159,8 @@
val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
val alarmManager = makeMockAlarmManager()
val systemConfigManager = makeMockSystemConfigManager()
- val batteryManager = BatteryStatsManager(mock<IBatteryStats>())
+ val batteryStats = mock<IBatteryStats>()
+ val batteryManager = BatteryStatsManager(batteryStats)
val telephonyManager = mock<TelephonyManager>().also {
doReturn(true).`when`(it).isDataCapable()
}
@@ -195,6 +201,8 @@
// checking permissions.
override fun isFeatureEnabled(context: Context?, name: String?) =
enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
+ override fun isFeatureNotChickenedOut(context: Context?, name: String?) =
+ enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
// Mocked change IDs
private val enabledChangeIds = ArraySet<Long>()
@@ -282,6 +290,26 @@
Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
else -> super.getSystemService(serviceName)
}
+
+ internal val orderedBroadcastAsUserHistory = ArrayTrackRecord<Intent>().newReadHead()
+
+ fun expectNoDataActivityBroadcast(timeoutMs: Int) {
+ assertNull(orderedBroadcastAsUserHistory.poll(
+ timeoutMs.toLong()) { intent -> true })
+ }
+
+ override fun sendOrderedBroadcastAsUser(
+ intent: Intent,
+ user: UserHandle,
+ receiverPermission: String?,
+ resultReceiver: BroadcastReceiver?,
+ scheduler: Handler?,
+ initialCode: Int,
+ initialData: String?,
+ initialExtras: Bundle?
+ ) {
+ orderedBroadcastAsUserHistory.add(intent)
+ }
}
// Utility methods for subclasses to use
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 92a5b64..7a4dfed 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -193,6 +193,7 @@
* TODO: This test used to be really brittle because it used Easymock - it uses Mockito now, but
* still uses the Easymock structure, which could be simplified.
*/
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
// NetworkStatsService is not updatable before T, so tests do not need to be backwards compatible
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.java b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
index 9cfd0b8..c1351af 100644
--- a/thread/framework/java/android/net/thread/PendingOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
@@ -39,6 +39,7 @@
* a given delay. This is typically used to deploy new network parameters (e.g. Network Key or
* Channel) to all devices in the network.
*
+ * @see ThreadNetworkController#scheduleMigration
* @hide
*/
@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@@ -76,7 +77,8 @@
* @param pendingTimestamp the Pending Timestamp which represents the version of this Pending
* Dataset
* @param delayTimer the delay after when {@code activeOpDataset} will be committed on this
- * device
+ * device; use {@link Duration#ZERO} to tell the system to choose a reasonable value
+ * automatically
*/
public PendingOperationalDataset(
@NonNull ActiveOperationalDataset activeOpDataset,
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index ec39db4..5c5fda9 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -226,15 +226,17 @@
* @param callback the callback which has been registered with {@link #registerStateCallback}
* @throws IllegalArgumentException if {@code callback} hasn't been registered
*/
+ @RequiresPermission(permission.ACCESS_NETWORK_STATE)
public void unregisterStateCallback(@NonNull StateCallback callback) {
requireNonNull(callback, "callback cannot be null");
synchronized (mStateCallbackMapLock) {
- StateCallbackProxy callbackProxy = mStateCallbackMap.remove(callback);
+ StateCallbackProxy callbackProxy = mStateCallbackMap.get(callback);
if (callbackProxy == null) {
throw new IllegalArgumentException("callback hasn't been registered");
}
try {
mControllerService.unregisterStateCallback(callbackProxy);
+ mStateCallbackMap.remove(callback);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
@@ -334,15 +336,21 @@
* #registerOperationalDatasetCallback}
* @throws IllegalArgumentException if {@code callback} hasn't been registered
*/
+ @RequiresPermission(
+ allOf = {
+ permission.ACCESS_NETWORK_STATE,
+ "android.permission.THREAD_NETWORK_PRIVILEGED"
+ })
public void unregisterOperationalDatasetCallback(@NonNull OperationalDatasetCallback callback) {
requireNonNull(callback, "callback cannot be null");
synchronized (mOpDatasetCallbackMapLock) {
- OperationalDatasetCallbackProxy callbackProxy = mOpDatasetCallbackMap.remove(callback);
+ OperationalDatasetCallbackProxy callbackProxy = mOpDatasetCallbackMap.get(callback);
if (callbackProxy == null) {
throw new IllegalArgumentException("callback hasn't been registered");
}
try {
mControllerService.unregisterOperationalDatasetCallback(callbackProxy);
+ mOpDatasetCallbackMap.remove(callback);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
new file mode 100644
index 0000000..d7c49a0
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import android.os.ParcelFileDescriptor;
+
+import java.io.IOException;
+
+/** Controller for the infrastructure network interface. */
+public class InfraInterfaceController {
+ private static final String TAG = "InfraIfController";
+
+ static {
+ System.loadLibrary("service-thread-jni");
+ }
+
+ /**
+ * Creates a socket on the infrastructure network interface for sending/receiving ICMPv6
+ * Neighbor Discovery messages.
+ *
+ * @param infraInterfaceName the infrastructure network interface name.
+ * @return an ICMPv6 socket file descriptor on the Infrastructure network interface.
+ * @throws IOException when fails to create the socket.
+ */
+ public static ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName)
+ throws IOException {
+ return ParcelFileDescriptor.adoptFd(nativeCreateIcmp6Socket(infraInterfaceName));
+ }
+
+ private static native int nativeCreateIcmp6Socket(String interfaceName) throws IOException;
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 6c9a775..33516aa 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -223,6 +223,7 @@
return mOtDaemon;
}
+ // TODO(b/309792480): restarts the OT daemon service
private void onOtDaemonDied() {
Log.w(TAG, "OT daemon became dead, clean up...");
OperationReceiverWrapper.onOtDaemonDied();
@@ -418,12 +419,12 @@
@Override
public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
-
mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
}
@Override
public void unregisterStateCallback(IStateCallback stateCallback) throws RemoteException {
+ enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
mHandler.post(() -> mOtDaemonCallbackProxy.unregisterStateCallback(stateCallback));
}
@@ -438,6 +439,8 @@
@Override
public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback)
throws RemoteException {
+ enforceAllCallingPermissionsGranted(
+ permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback));
}
@@ -566,9 +569,15 @@
private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
if (ThreadNetworkController.isAttached(deviceRole)) {
Log.d(TAG, "Attached to the Thread network");
+
+ // This is an idempotent method which can be called for multiple times when the device
+ // is already attached (e.g. going from Child to Router)
registerThreadNetwork();
} else {
Log.d(TAG, "Detached from the Thread network");
+
+ // This is an idempotent method which can be called for multiple times when the device
+ // is already detached or stopped
unregisterThreadNetwork();
}
}
@@ -625,7 +634,7 @@
public void registerStateCallback(IStateCallback callback) {
checkOnHandlerThread();
if (mStateCallbacks.containsKey(callback)) {
- return;
+ throw new IllegalStateException("Registering the same IStateCallback twice");
}
IBinder.DeathRecipient deathRecipient =
@@ -657,7 +666,8 @@
public void registerDatasetCallback(IOperationalDatasetCallback callback) {
checkOnHandlerThread();
if (mOpDatasetCallbacks.containsKey(callback)) {
- return;
+ throw new IllegalStateException(
+ "Registering the same IOperationalDatasetCallback twice");
}
IBinder.DeathRecipient deathRecipient =
@@ -732,7 +742,7 @@
mActiveDataset = newActiveDataset;
} catch (IllegalArgumentException e) {
// Is unlikely that OT will generate invalid Operational Dataset
- Log.w(TAG, "Ignoring invalid Active Operational Dataset changes", e);
+ Log.wtf(TAG, "Invalid Active Operational Dataset from OpenThread", e);
}
PendingOperationalDataset newPendingDataset;
@@ -746,7 +756,8 @@
onPendingOperationalDatasetChanged(newPendingDataset, listenerId);
mPendingDataset = newPendingDataset;
} catch (IllegalArgumentException e) {
- Log.w(TAG, "Ignoring invalid Pending Operational Dataset changes", e);
+ // Is unlikely that OT will generate invalid Operational Dataset
+ Log.wtf(TAG, "Invalid Pending Operational Dataset from OpenThread", e);
}
}
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index ac65b11..7223b2a 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -16,6 +16,7 @@
package com.android.server.thread;
+import android.annotation.Nullable;
import android.net.LinkAddress;
import android.net.util.SocketUtils;
import android.os.ParcelFileDescriptor;
@@ -34,6 +35,7 @@
/** Controller for virtual/tunnel network interfaces. */
public class TunInterfaceController {
private static final String TAG = "TunIfController";
+ private static final long INFINITE_LIFETIME = 0xffffffffL;
static final int MTU = 1280;
static {
@@ -76,6 +78,7 @@
}
/** Returns the FD of the tunnel interface. */
+ @Nullable
public ParcelFileDescriptor getTunFd() {
return mParcelTunFd;
}
@@ -98,7 +101,7 @@
if (address.getDeprecationTime() == LinkAddress.LIFETIME_PERMANENT
|| address.getDeprecationTime() == LinkAddress.LIFETIME_UNKNOWN) {
- validLifetimeSeconds = 0xffffffffL;
+ validLifetimeSeconds = INFINITE_LIFETIME;
} else {
validLifetimeSeconds =
Math.max(
@@ -108,7 +111,7 @@
if (address.getExpirationTime() == LinkAddress.LIFETIME_PERMANENT
|| address.getExpirationTime() == LinkAddress.LIFETIME_UNKNOWN) {
- preferredLifetimeSeconds = 0xffffffffL;
+ preferredLifetimeSeconds = INFINITE_LIFETIME;
} else {
preferredLifetimeSeconds =
Math.max(
diff --git a/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
new file mode 100644
index 0000000..5d24eab
--- /dev/null
+++ b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#define LOG_TAG "jniThreadInfra"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <ifaddrs.h>
+#include <inttypes.h>
+#include <linux/if_arp.h>
+#include <linux/ioctl.h>
+#include <log/log.h>
+#include <net/if.h>
+#include <netdb.h>
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <private/android_filesystem_config.h>
+#include <signal.h>
+#include <spawn.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "jni.h"
+#include "nativehelper/JNIHelp.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+namespace android {
+static jint
+com_android_server_thread_InfraInterfaceController_createIcmp6Socket(JNIEnv *env, jobject clazz,
+ jstring interfaceName) {
+ ScopedUtfChars ifName(env, interfaceName);
+
+ struct icmp6_filter filter;
+ constexpr int kEnable = 1;
+ constexpr int kIpv6ChecksumOffset = 2;
+ constexpr int kHopLimit = 255;
+
+ // Initializes the ICMPv6 socket.
+ int sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+ if (sock == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to create the socket (%s)",
+ strerror(errno));
+ return -1;
+ }
+
+ // Only accept Router Advertisements, Router Solicitations and Neighbor
+ // Advertisements.
+ ICMP6_FILTER_SETBLOCKALL(&filter);
+ ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &filter);
+ ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT, &filter);
+ ICMP6_FILTER_SETPASS(ND_NEIGHBOR_ADVERT, &filter);
+
+ if (setsockopt(sock, IPPROTO_ICMPV6, ICMP6_FILTER, &filter, sizeof(filter)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt ICMP6_FILTER (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ // We want a source address and interface index.
+
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &kEnable, sizeof(kEnable)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVPKTINFO (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ if (setsockopt(sock, IPPROTO_RAW, IPV6_CHECKSUM, &kIpv6ChecksumOffset,
+ sizeof(kIpv6ChecksumOffset)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_CHECKSUM (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ // We need to be able to reject RAs arriving from off-link.
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &kEnable, sizeof(kEnable)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVHOPLIMIT (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_UNICAST_HOPS (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "failed to create the setsockopt IPV6_MULTICAST_HOPS (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, ifName.c_str(), strlen(ifName.c_str()))) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt SO_BINDTODEVICE (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ return sock;
+}
+
+/*
+ * JNI registration.
+ */
+
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"nativeCreateIcmp6Socket", "(Ljava/lang/String;)I",
+ (void *)com_android_server_thread_InfraInterfaceController_createIcmp6Socket},
+};
+
+int register_com_android_server_thread_InfraInterfaceController(JNIEnv *env) {
+ return jniRegisterNativeMethods(env, "com/android/server/thread/InfraInterfaceController",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
index ed39fab..c56bc0b 100644
--- a/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
+++ b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
@@ -53,25 +53,25 @@
strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
if (ioctl(fd, TUNSETIFF, &ifr, sizeof(ifr)) != 0) {
- close(fd);
jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(TUNSETIFF) failed (%s)",
strerror(errno));
+ close(fd);
return -1;
}
int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
if (inet6 == -1) {
- close(fd);
jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
strerror(errno));
+ close(fd);
return -1;
}
ifr.ifr_mtu = mtu;
if (ioctl(inet6, SIOCSIFMTU, &ifr) != 0) {
- close(fd);
- close(inet6);
jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFMTU) failed (%s)",
strerror(errno));
+ close(fd);
+ close(inet6);
return -1;
}
@@ -94,7 +94,6 @@
}
if (ioctl(inet6, SIOCSIFFLAGS, &ifr) != 0) {
- close(inet6);
jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFFLAGS) failed (%s)",
strerror(errno));
}
diff --git a/thread/service/jni/onload.cpp b/thread/service/jni/onload.cpp
index 5081664..66add74 100644
--- a/thread/service/jni/onload.cpp
+++ b/thread/service/jni/onload.cpp
@@ -19,6 +19,7 @@
namespace android {
int register_com_android_server_thread_TunInterfaceController(JNIEnv* env);
+int register_com_android_server_thread_InfraInterfaceController(JNIEnv* env);
}
using namespace android;
@@ -33,5 +34,6 @@
ALOG_ASSERT(env != NULL, "Could not retrieve the env!");
register_com_android_server_thread_TunInterfaceController(env);
+ register_com_android_server_thread_InfraInterfaceController(env);
return JNI_VERSION_1_4;
}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index b75b8e6..6862398 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -37,6 +37,7 @@
"androidx.test.ext.junit",
"compatibility-device-util-axt",
"ctstestrunner-axt",
+ "guava",
"guava-android-testlib",
"net-tests-utils",
"truth",
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 e17dd02..362ff39 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -101,7 +101,7 @@
public void tearDown() throws Exception {
if (mManager != null) {
leaveAndWait();
- dropPermissions();
+ dropAllPermissions();
}
}
@@ -128,7 +128,7 @@
getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions);
}
- private static void dropPermissions() {
+ private static void dropAllPermissions() {
getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
}
@@ -217,16 +217,21 @@
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = deviceRole::set;
- controller.registerStateCallback(mExecutor, role -> deviceRole.set(role));
+ try {
+ controller.registerStateCallback(mExecutor, callback);
- assertThat(deviceRole.get()).isEqualTo(DEVICE_ROLE_STOPPED);
+ assertThat(deviceRole.get()).isEqualTo(DEVICE_ROLE_STOPPED);
+ } finally {
+ controller.unregisterStateCallback(callback);
+ }
}
}
@Test
public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
- dropPermissions();
+ dropAllPermissions();
for (ThreadNetworkController controller : getAllControllers()) {
assertThrows(
@@ -252,6 +257,26 @@
}
@Test
+ public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = role -> deviceRole.set(role);
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ controller.registerStateCallback(mExecutor, callback);
+
+ try {
+ dropAllPermissions();
+ assertThrows(
+ SecurityException.class,
+ () -> controller.unregisterStateCallback(callback));
+ } finally {
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ controller.unregisterStateCallback(callback);
+ }
+ }
+ }
+
+ @Test
public void unregisterStateCallback_callbackRegistered_success() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE);
for (ThreadNetworkController controller : getAllControllers()) {
@@ -282,7 +307,7 @@
grantPermissions(permission.ACCESS_NETWORK_STATE);
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<Integer> deviceRole = SettableFuture.create();
- StateCallback callback = role -> deviceRole.set(role);
+ StateCallback callback = deviceRole::set;
controller.registerStateCallback(mExecutor, callback);
controller.unregisterStateCallback(callback);
@@ -300,12 +325,70 @@
for (ThreadNetworkController controller : getAllControllers()) {
SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
- controller.registerOperationalDatasetCallback(
- mExecutor, newDatasetCallback(activeFuture, pendingFuture));
+ try {
+ controller.registerOperationalDatasetCallback(mExecutor, callback);
- assertThat(activeFuture.get()).isNull();
- assertThat(pendingFuture.get()).isNull();
+ assertThat(activeFuture.get()).isNull();
+ assertThat(pendingFuture.get()).isNull();
+ } finally {
+ controller.unregisterOperationalDatasetCallback(callback);
+ }
+ }
+ }
+
+ @Test
+ public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+ assertThrows(
+ SecurityException.class,
+ () -> controller.registerOperationalDatasetCallback(mExecutor, callback));
+ }
+ }
+
+ @Test
+ public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+ controller.registerOperationalDatasetCallback(mExecutor, callback);
+
+ controller.unregisterOperationalDatasetCallback(callback);
+ }
+ }
+
+ @Test
+ public void unregisterOperationalDatasetCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ controller.registerOperationalDatasetCallback(mExecutor, callback);
+
+ try {
+ dropAllPermissions();
+ assertThrows(
+ SecurityException.class,
+ () -> controller.unregisterOperationalDatasetCallback(callback));
+ } finally {
+ grantPermissions(
+ permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ controller.unregisterOperationalDatasetCallback(callback);
+ }
}
}
@@ -343,7 +426,7 @@
@Test
public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
- dropPermissions();
+ dropAllPermissions();
for (ThreadNetworkController controller : getAllControllers()) {
ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
@@ -408,7 +491,7 @@
@Test
public void leave_withoutPrivilegedPermission_throwsSecurityException() {
- dropPermissions();
+ dropAllPermissions();
for (ThreadNetworkController controller : getAllControllers()) {
assertThrows(SecurityException.class, () -> controller.leave(mExecutor, v -> {}));
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 3a087c7..5863673 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -36,6 +36,7 @@
"ctstestrunner-axt",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
+ "guava",
"guava-android-testlib",
"net-tests-utils",
"truth",